add Frame实现和测试
This commit is contained in:
parent
1300b62958
commit
bf1ab04be1
|
|
@ -0,0 +1,371 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== 媒体类型定义 ====================
|
||||||
|
|
||||||
|
type TrackType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TrackInvalid TrackType = iota - 1
|
||||||
|
TrackVideo
|
||||||
|
TrackAudio
|
||||||
|
TrackTitle
|
||||||
|
TrackApplication
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t TrackType) String() string {
|
||||||
|
switch t {
|
||||||
|
case TrackVideo:
|
||||||
|
return "video"
|
||||||
|
case TrackAudio:
|
||||||
|
return "audio"
|
||||||
|
case TrackApplication:
|
||||||
|
return "application"
|
||||||
|
default:
|
||||||
|
return "invalid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTrackType(str string) TrackType {
|
||||||
|
switch str {
|
||||||
|
case "video":
|
||||||
|
return TrackVideo
|
||||||
|
case "audio":
|
||||||
|
return TrackAudio
|
||||||
|
case "application":
|
||||||
|
return TrackApplication
|
||||||
|
default:
|
||||||
|
return TrackInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 编解码标识 ====================
|
||||||
|
|
||||||
|
type CodecID int
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodecInvalid CodecID = iota - 1
|
||||||
|
CodecH264
|
||||||
|
CodecH265
|
||||||
|
CodecAAC
|
||||||
|
CodecG711A
|
||||||
|
CodecG711U
|
||||||
|
CodecOpus
|
||||||
|
// ... 其他编解码器定义
|
||||||
|
)
|
||||||
|
|
||||||
|
var codecNames = map[CodecID]string{
|
||||||
|
CodecH264: "H264",
|
||||||
|
CodecH265: "H265",
|
||||||
|
CodecAAC: "mpeg4-generic",
|
||||||
|
CodecG711A: "PCMA",
|
||||||
|
CodecG711U: "PCMU",
|
||||||
|
CodecOpus: "opus",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CodecID) String() string {
|
||||||
|
if name, ok := codecNames[c]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "invalid"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCodecID(name string) CodecID {
|
||||||
|
for cid, n := range codecNames {
|
||||||
|
if n == name {
|
||||||
|
return cid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CodecInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 帧接口定义 ====================
|
||||||
|
|
||||||
|
type Frame interface {
|
||||||
|
DTS() uint64
|
||||||
|
PTS() uint64
|
||||||
|
PrefixSize() int
|
||||||
|
KeyFrame() bool
|
||||||
|
ConfigFrame() bool
|
||||||
|
CacheAble() bool
|
||||||
|
DropAble() bool
|
||||||
|
DecodeAble() bool
|
||||||
|
Data() []byte
|
||||||
|
Size() int
|
||||||
|
CodecID() CodecID
|
||||||
|
TrackType() TrackType
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 基础帧实现 ====================
|
||||||
|
|
||||||
|
type BaseFrame struct {
|
||||||
|
Dts uint64
|
||||||
|
Pts uint64
|
||||||
|
VPrefixSize int
|
||||||
|
Codec CodecID
|
||||||
|
Track TrackType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BaseFrame) DTS() uint64 { return f.Dts }
|
||||||
|
func (f *BaseFrame) PTS() uint64 { return f.Pts }
|
||||||
|
func (f *BaseFrame) PrefixSize() int { return f.VPrefixSize }
|
||||||
|
func (f *BaseFrame) CodecID() CodecID { return f.Codec }
|
||||||
|
func (f *BaseFrame) TrackType() TrackType { return f.Track }
|
||||||
|
|
||||||
|
// ==================== 帧包装器 ====================
|
||||||
|
|
||||||
|
type FrameFromBytes struct {
|
||||||
|
*BaseFrame
|
||||||
|
DataPtr []byte
|
||||||
|
IsKey bool
|
||||||
|
IsConfig bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FrameFromBytes) KeyFrame() bool { return f.IsKey }
|
||||||
|
func (f *FrameFromBytes) ConfigFrame() bool { return f.IsConfig }
|
||||||
|
func (f *FrameFromBytes) CacheAble() bool { return false }
|
||||||
|
func (f *FrameFromBytes) DropAble() bool { return false }
|
||||||
|
func (f *FrameFromBytes) DecodeAble() bool {
|
||||||
|
if f.TrackType() != TrackVideo {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !f.ConfigFrame()
|
||||||
|
}
|
||||||
|
func (f *FrameFromBytes) Data() []byte { return f.DataPtr }
|
||||||
|
func (f *FrameFromBytes) Size() int { return len(f.DataPtr) }
|
||||||
|
|
||||||
|
// ==================== 可缓存帧 ====================
|
||||||
|
|
||||||
|
type FrameCacheAble struct {
|
||||||
|
*FrameFromBytes
|
||||||
|
Buffer []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFrameCacheAble(frame Frame, forceKeyFrame bool) Frame {
|
||||||
|
if frame.CacheAble() {
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]byte, frame.Size())
|
||||||
|
copy(buffer, frame.Data())
|
||||||
|
|
||||||
|
return &FrameCacheAble{
|
||||||
|
FrameFromBytes: &FrameFromBytes{
|
||||||
|
BaseFrame: &BaseFrame{
|
||||||
|
Dts: frame.DTS(),
|
||||||
|
Pts: frame.PTS(),
|
||||||
|
VPrefixSize: frame.PrefixSize(),
|
||||||
|
Codec: frame.CodecID(),
|
||||||
|
Track: frame.TrackType(),
|
||||||
|
},
|
||||||
|
DataPtr: buffer,
|
||||||
|
IsKey: forceKeyFrame || frame.KeyFrame(),
|
||||||
|
IsConfig: frame.ConfigFrame(),
|
||||||
|
},
|
||||||
|
Buffer: buffer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FrameCacheAble) CacheAble() bool { return true }
|
||||||
|
|
||||||
|
// ==================== 时间戳修正 ====================
|
||||||
|
|
||||||
|
type FrameStamp struct {
|
||||||
|
Frame
|
||||||
|
Dts int64
|
||||||
|
Pts int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFrameStamp(frame Frame, dts, pts int64) Frame {
|
||||||
|
return &FrameStamp{Frame: frame, Dts: dts, Pts: pts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FrameStamp) DTS() uint64 { return uint64(f.Dts) }
|
||||||
|
func (f *FrameStamp) PTS() uint64 { return uint64(f.Pts) }
|
||||||
|
|
||||||
|
// ==================== 帧合并器 ====================
|
||||||
|
|
||||||
|
type FrameMerger struct {
|
||||||
|
frameCache []Frame
|
||||||
|
haveDecodeable bool
|
||||||
|
mergeType int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MergeNone = iota
|
||||||
|
MergeH264Prefix
|
||||||
|
MergeMP4NalSize
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFrameMerger(mergeType int) *FrameMerger {
|
||||||
|
return &FrameMerger{mergeType: mergeType}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FrameMerger) WillFlush(frame Frame) bool {
|
||||||
|
if len(m.frameCache) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if frame == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
last := m.frameCache[len(m.frameCache)-1]
|
||||||
|
switch m.mergeType {
|
||||||
|
case MergeNone:
|
||||||
|
return last.DTS() != frame.DTS() || len(m.frameCache) > 100
|
||||||
|
case MergeH264Prefix, MergeMP4NalSize:
|
||||||
|
return last.DTS() != frame.DTS() || frame.DecodeAble() || frame.ConfigFrame() || len(m.frameCache) > 100
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FrameMerger) DoMerge(buffer *[]byte, frame Frame) {
|
||||||
|
switch m.mergeType {
|
||||||
|
case MergeNone:
|
||||||
|
*buffer = append(*buffer, frame.Data()...)
|
||||||
|
case MergeH264Prefix:
|
||||||
|
if frame.PrefixSize() == 0 {
|
||||||
|
*buffer = append(*buffer, 0, 0, 0, 1)
|
||||||
|
}
|
||||||
|
// 确保每个NAL单元都有独立的起始码
|
||||||
|
*buffer = append(*buffer, frame.Data()...)
|
||||||
|
case MergeMP4NalSize:
|
||||||
|
size := uint32(frame.Size() - frame.PrefixSize())
|
||||||
|
*buffer = append(*buffer, byte(size>>24), byte(size>>16), byte(size>>8), byte(size))
|
||||||
|
*buffer = append(*buffer, frame.Data()[frame.PrefixSize():]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FrameMerger) InputFrame(frame Frame, cb func(dts, pts uint64, buffer []byte, haveKeyFrame bool)) bool {
|
||||||
|
if frame == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.DecodeAble() {
|
||||||
|
m.haveDecodeable = true
|
||||||
|
}
|
||||||
|
m.frameCache = append(m.frameCache, frame)
|
||||||
|
|
||||||
|
if frame != nil && !m.NeedMerge(frame.CodecID()) {
|
||||||
|
cb(frame.DTS(), frame.PTS(), frame.Data(), frame.KeyFrame())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.WillFlush(frame) {
|
||||||
|
last := m.frameCache[len(m.frameCache)-1]
|
||||||
|
var buffer []byte
|
||||||
|
var haveKeyFrame = last.KeyFrame()
|
||||||
|
|
||||||
|
if len(m.frameCache) > 1 || m.mergeType == MergeMP4NalSize {
|
||||||
|
buffer = make([]byte, 0, last.Size()+1024)
|
||||||
|
for _, f := range m.frameCache {
|
||||||
|
m.DoMerge(&buffer, f)
|
||||||
|
if f.KeyFrame() {
|
||||||
|
haveKeyFrame = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(last.DTS(), last.PTS(), buffer, haveKeyFrame)
|
||||||
|
m.frameCache = m.frameCache[:0]
|
||||||
|
m.haveDecodeable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FrameMerger) NeedMerge(codec CodecID) bool {
|
||||||
|
return codec == CodecH264 || codec == CodecH265
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *FrameMerger) Flush(cb func(dts, pts uint64, buffer []byte, haveKeyFrame bool)) {
|
||||||
|
if len(m.frameCache) > 0 {
|
||||||
|
m.InputFrame(nil, cb)
|
||||||
|
}
|
||||||
|
m.frameCache = m.frameCache[:0]
|
||||||
|
m.haveDecodeable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 帧分发器 ====================
|
||||||
|
|
||||||
|
type FrameDispatcher struct {
|
||||||
|
delegates map[*FrameWriter]bool
|
||||||
|
mtx sync.Mutex
|
||||||
|
// 统计信息
|
||||||
|
videoKeyFrames uint64
|
||||||
|
frames uint64
|
||||||
|
lastFrames uint64
|
||||||
|
gopSize uint64
|
||||||
|
gopInterval time.Duration
|
||||||
|
lastKeyFrameTS uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFrameDispatcher() *FrameDispatcher {
|
||||||
|
return &FrameDispatcher{delegates: make(map[*FrameWriter]bool)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FrameDispatcher) AddDelegate(w *FrameWriter) {
|
||||||
|
d.mtx.Lock()
|
||||||
|
defer d.mtx.Unlock()
|
||||||
|
d.delegates[w] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FrameDispatcher) DelDelegate(w *FrameWriter) {
|
||||||
|
d.mtx.Lock()
|
||||||
|
defer d.mtx.Unlock()
|
||||||
|
delete(d.delegates, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FrameDispatcher) InputFrame(frame Frame) bool {
|
||||||
|
if !frame.ConfigFrame() && !frame.DropAble() {
|
||||||
|
d.frames++
|
||||||
|
if frame.KeyFrame() && frame.TrackType() == TrackVideo {
|
||||||
|
d.videoKeyFrames++
|
||||||
|
d.gopSize = d.frames - d.lastFrames
|
||||||
|
d.gopInterval = time.Duration(frame.DTS()-d.lastKeyFrameTS) * time.Millisecond
|
||||||
|
d.lastFrames = d.frames
|
||||||
|
d.lastKeyFrameTS = frame.DTS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mtx.Lock()
|
||||||
|
defer d.mtx.Unlock()
|
||||||
|
|
||||||
|
var ret bool
|
||||||
|
for w := range d.delegates {
|
||||||
|
if w.InputFrame(frame) {
|
||||||
|
ret = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FrameDispatcher) GetStats() (uint64, uint64, uint64, time.Duration) {
|
||||||
|
return d.frames, d.videoKeyFrames, d.gopSize, d.gopInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size 返回当前注册的代理数量
|
||||||
|
// Size returns the number of registered delegates
|
||||||
|
func (d *FrameDispatcher) Size() int {
|
||||||
|
d.mtx.Lock()
|
||||||
|
defer d.mtx.Unlock()
|
||||||
|
return len(d.delegates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 帧写入接口 ====================
|
||||||
|
|
||||||
|
type FrameWriter struct {
|
||||||
|
OnFrame func(Frame) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *FrameWriter) InputFrame(frame Frame) bool {
|
||||||
|
if w.OnFrame != nil {
|
||||||
|
return w.OnFrame(frame)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
package extension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrackTypeConversion(t *testing.T) {
|
||||||
|
// 验证字符串转TrackType
|
||||||
|
if GetTrackType("video") != TrackVideo {
|
||||||
|
t.Error("video track type conversion failed")
|
||||||
|
}
|
||||||
|
if GetTrackType("invalid") != TrackInvalid {
|
||||||
|
t.Error("invalid track type conversion failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证TrackType转字符串
|
||||||
|
if TrackVideo.String() != "video" {
|
||||||
|
t.Error("TrackVideo string conversion failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCodecIDConversion(t *testing.T) {
|
||||||
|
// 验证名称转CodecID
|
||||||
|
if GetCodecID("H264") != CodecH264 {
|
||||||
|
t.Error("H264 codec conversion failed")
|
||||||
|
}
|
||||||
|
if GetCodecID("invalid") != CodecInvalid {
|
||||||
|
t.Error("invalid codec conversion failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证CodecID转名称
|
||||||
|
if CodecH264.String() != "H264" {
|
||||||
|
t.Error("CodecH264 string conversion failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseFrame(t *testing.T) {
|
||||||
|
frame := &BaseFrame{
|
||||||
|
Dts: 1000,
|
||||||
|
Pts: 1005,
|
||||||
|
VPrefixSize: 4,
|
||||||
|
Codec: CodecH264,
|
||||||
|
Track: TrackVideo,
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.DTS() != 1000 {
|
||||||
|
t.Error("DTS mismatch")
|
||||||
|
}
|
||||||
|
if frame.PrefixSize() != 4 {
|
||||||
|
t.Error("PrefixSize mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrameCacheAble(t *testing.T) {
|
||||||
|
// 测试原始帧不可缓存
|
||||||
|
raw := &FrameFromBytes{
|
||||||
|
BaseFrame: &BaseFrame{Dts: 1000},
|
||||||
|
DataPtr: []byte{1, 2, 3},
|
||||||
|
}
|
||||||
|
if raw.CacheAble() {
|
||||||
|
t.Error("raw frame should not be cacheable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试转换后可缓存
|
||||||
|
cached := NewFrameCacheAble(raw, false)
|
||||||
|
if !cached.CacheAble() {
|
||||||
|
t.Error("cached frame should be cacheable")
|
||||||
|
}
|
||||||
|
if cached.Size() != 3 {
|
||||||
|
t.Error("data size mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrameMerger_H264Prefix(t *testing.T) {
|
||||||
|
merger := NewFrameMerger(MergeH264Prefix)
|
||||||
|
var output []byte
|
||||||
|
|
||||||
|
// 创建测试帧(无前缀)
|
||||||
|
frame1 := &FrameFromBytes{BaseFrame: &BaseFrame{Dts: 1000, VPrefixSize: 0}, DataPtr: []byte{0x67}}
|
||||||
|
frame2 := &FrameFromBytes{BaseFrame: &BaseFrame{Dts: 1000, VPrefixSize: 0}, DataPtr: []byte{0x68}}
|
||||||
|
|
||||||
|
// 合并相同时间戳的帧
|
||||||
|
merger.InputFrame(frame1, func(dts, pts uint64, buf []byte, key bool) {
|
||||||
|
output = append(output, buf...)
|
||||||
|
})
|
||||||
|
merger.InputFrame(frame2, func(dts, pts uint64, buf []byte, key bool) {
|
||||||
|
output = append(output, buf...)
|
||||||
|
})
|
||||||
|
merger.Flush(func(dts, pts uint64, buf []byte, key bool) {
|
||||||
|
output = append(output, buf...)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证H264前缀添加
|
||||||
|
expected := []byte{0, 0, 0, 1, 0x67, 0, 0, 0, 1, 0x68}
|
||||||
|
if string(output) != string(expected) {
|
||||||
|
t.Error("H264 prefix merge failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrameMerger_MP4NalSize(t *testing.T) {
|
||||||
|
merger := NewFrameMerger(MergeMP4NalSize)
|
||||||
|
var output []byte
|
||||||
|
|
||||||
|
// 创建测试帧(带4字节前缀)
|
||||||
|
frame := &FrameFromBytes{
|
||||||
|
BaseFrame: &BaseFrame{VPrefixSize: 4, Dts: 1000},
|
||||||
|
DataPtr: []byte{0, 0, 0, 1, 0x67},
|
||||||
|
}
|
||||||
|
|
||||||
|
merger.InputFrame(frame, func(dts, pts uint64, buf []byte, key bool) {
|
||||||
|
output = buf
|
||||||
|
})
|
||||||
|
merger.Flush(func(dts, pts uint64, buf []byte, key bool) {
|
||||||
|
output = buf
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证MP4 NALU大小(4字节长度 + 原始数据)
|
||||||
|
if len(output) != 5 {
|
||||||
|
t.Error("MP4 nal size length mismatch")
|
||||||
|
}
|
||||||
|
if output[0] != 0 || output[1] != 0 || output[2] != 0 || output[3] != 1 {
|
||||||
|
t.Error("MP4 nal size header mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrameDispatcher(t *testing.T) {
|
||||||
|
dispatcher := NewFrameDispatcher()
|
||||||
|
statsChan := make(chan struct{})
|
||||||
|
|
||||||
|
// 添加测试代理
|
||||||
|
dispatcher.AddDelegate(&FrameWriter{
|
||||||
|
OnFrame: func(frame Frame) bool {
|
||||||
|
if frame.KeyFrame() && frame.TrackType() == TrackVideo {
|
||||||
|
close(statsChan)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 输入视频关键帧
|
||||||
|
dispatcher.InputFrame(&FrameFromBytes{
|
||||||
|
BaseFrame: &BaseFrame{
|
||||||
|
Dts: 1000,
|
||||||
|
Track: TrackVideo,
|
||||||
|
},
|
||||||
|
IsKey: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证GOP统计
|
||||||
|
select {
|
||||||
|
case <-statsChan:
|
||||||
|
frames, keys, gopSize, _ := dispatcher.GetStats()
|
||||||
|
if frames != 1 || keys != 1 || gopSize != 1 {
|
||||||
|
t.Error("GOP statistics mismatch")
|
||||||
|
}
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Error("timeout waiting for stats")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrameDispatcher_Concurrent(t *testing.T) {
|
||||||
|
dispatcher := NewFrameDispatcher()
|
||||||
|
var done sync.WaitGroup
|
||||||
|
|
||||||
|
// 并发添加/删除代理
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
done.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer done.Done()
|
||||||
|
writer := &FrameWriter{}
|
||||||
|
dispatcher.AddDelegate(writer)
|
||||||
|
dispatcher.DelDelegate(writer)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
done.Wait()
|
||||||
|
|
||||||
|
// 验证线程安全
|
||||||
|
if dispatcher.Size() != 0 {
|
||||||
|
t.Error("concurrent delegate management failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue