From 709e56166f2b3433c0d3b15c2d73ebb647619658 Mon Sep 17 00:00:00 2001 From: Lukas Herman Date: Tue, 24 Dec 2019 22:43:38 -0800 Subject: [PATCH] Completely rearchitect the overall project structure --- camera/camera_linux.go | 160 ---------------------------- camera/const.go | 5 - camera/opt.go | 11 -- docs/drawio/driver-lifecycle.drawio | 1 + driver/camera_linux.go | 107 +++++++++++++++++++ driver/const.go | 8 ++ driver/driver.go | 52 +++++++++ driver/manager.go | 31 ++++++ examples/simple/main.go | 4 +- go.mod | 1 + go.sum | 2 + mediadevices.go | 20 ++-- mediastream.go | 54 +++++----- sampler.go | 28 +++++ track.go | 101 ++++++++++++++++++ 15 files changed, 371 insertions(+), 214 deletions(-) delete mode 100644 camera/camera_linux.go delete mode 100644 camera/const.go delete mode 100644 camera/opt.go create mode 100644 docs/drawio/driver-lifecycle.drawio create mode 100644 driver/camera_linux.go create mode 100644 driver/const.go create mode 100644 driver/driver.go create mode 100644 driver/manager.go create mode 100644 sampler.go create mode 100644 track.go diff --git a/camera/camera_linux.go b/camera/camera_linux.go deleted file mode 100644 index d658a0f..0000000 --- a/camera/camera_linux.go +++ /dev/null @@ -1,160 +0,0 @@ -package camera - -// #include -import "C" - -import ( - "fmt" - "math/rand" - "time" - - "github.com/blackjack/webcam" - codecEngine "github.com/pion/codec" - "github.com/pion/codec/h264" - "github.com/pion/mediadevices/frame" - "github.com/pion/webrtc/v2" - "github.com/pion/webrtc/v2/pkg/media" -) - -var supportedFormats = map[webcam.PixelFormat]frame.Format{ - webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV): frame.FormatYUYV, - webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV21, - webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG, -} - -// Camera implementation using v4l2 -// Reference: https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/videodev.html#videodev -type Camera struct { - cam *webcam.Webcam - track *webrtc.Track - encoder codecEngine.Encoder - decoder frame.Decoder - opts Options -} - -func New(opts Options) (*Camera, error) { - cam, err := webcam.Open("/dev/video0") - if err != nil { - return nil, err - } - - width := opts.Width - height := opts.Height - - var selectedFormat webcam.PixelFormat - camFormats := cam.GetSupportedFormats() - for format := range supportedFormats { - if _, ok := camFormats[format]; ok { - selectedFormat = format - break - } - } - - if selectedFormat == 0 { - msg := "your camera is not currently supported." - // TODO: Make it prettier - msg = fmt.Sprintf("%s\nyours: %v\nsupported:%v", msg, camFormats, supportedFormats) - return nil, fmt.Errorf(msg) - } - - if _, _, _, err = cam.SetImageFormat(selectedFormat, uint32(width), uint32(height)); err != nil { - return nil, err - } - - decoder, err := frame.NewDecoder(supportedFormats[selectedFormat]) - if err != nil { - return nil, err - } - - var c *Camera - switch opts.Codec { - case webrtc.H264: - // TODO: Replace "pion1" with device id instead - track, err := opts.PC.NewTrack(webrtc.DefaultPayloadTypeH264, rand.Uint32(), "video", "pion1") - if err != nil { - return nil, err - } - // TODO: Remove hardcoded values - encoder, err := h264.NewEncoder(h264.Options{ - Width: width, - Height: height, - MaxFrameRate: 30, - Bitrate: 1000000, - }) - if err != nil { - return nil, err - } - c = &Camera{ - cam: cam, - track: track, - encoder: encoder, - decoder: decoder, - opts: opts, - } - default: - return nil, fmt.Errorf("%s is not currently supported", opts.Codec) - } - - return c, nil -} - -func (c *Camera) Start() error { - if err := c.cam.StartStreaming(); err != nil { - return err - } - - lastTimestamp := time.Now() - for { - err := c.cam.WaitForFrame(5) - switch err.(type) { - case nil: - case *webcam.Timeout: - continue - default: - return err - } - - frame, err := c.cam.ReadFrame() - if err != nil { - // TODO: Add a better error handling - return err - } - - if len(frame) == 0 { - continue - } - - img, err := c.decoder.Decode(frame, c.opts.Width, c.opts.Height) - if err != nil { - continue - } - - encoded, err := c.encoder.Encode(img) - if err != nil { - // TODO: Add a better error handling - return err - } - - now := time.Now() - duration := now.Sub(lastTimestamp).Seconds() - samples := uint32(clockRate * duration) - lastTimestamp = now - - if err := c.track.WriteSample(media.Sample{Data: encoded, Samples: samples}); err != nil { - // TODO: Add a better error handling - continue - } - } -} - -func (c *Camera) Track() *webrtc.Track { - return c.track -} - -func (c *Camera) Stop() { - if c.cam == nil { - return - } - - c.cam.StopStreaming() -} diff --git a/camera/const.go b/camera/const.go deleted file mode 100644 index a80fb2d..0000000 --- a/camera/const.go +++ /dev/null @@ -1,5 +0,0 @@ -package camera - -const ( - clockRate = 90000 -) diff --git a/camera/opt.go b/camera/opt.go deleted file mode 100644 index 447a4fd..0000000 --- a/camera/opt.go +++ /dev/null @@ -1,11 +0,0 @@ -package camera - -import ( - "github.com/pion/webrtc/v2" -) - -type Options struct { - PC *webrtc.PeerConnection - Codec string - Width, Height int -} diff --git a/docs/drawio/driver-lifecycle.drawio b/docs/drawio/driver-lifecycle.drawio new file mode 100644 index 0000000..84c0a80 --- /dev/null +++ b/docs/drawio/driver-lifecycle.drawio @@ -0,0 +1 @@ +7VrJkpswEP0aHz0Fktfj2DNJqpJUUvEhyVE2MpAAIkIe2/n6tEAsYvESjzGuxBej1oJEv+7X3dDDc3/3lpPQ+cgs6vWQYe16+KmHkGmaQ/iTkn0iGWGcCGzuWmpQLli4v6kSGkq6cS0aaQMFY55wQ124YkFAV0KTEc7ZVh+2Zp5+15DYtCJYrIhXlX51LeEk0snQyOXvqGs76Z1NQ/X4JB2sBJFDLLYtiPBzD885YyK58ndz6smHlz6XZN6bht5sY5wG4pQJn9+4nv30e/HD+IDe+svtly/++745UJsT+/TE1IIHoJqMC4fZLCDecy6dcbYJLCqXNaCVj/nAWAhCE4Q/qBB7pU2yEQxEjvA91Ut3rvhWuP4ul3oYqtbTTq0cN/aqkexTbq7x/EoUsQ1f0UOHVjgi3KbiwDiUaQngTZlPBd/DPE49ItwXfR9E4czOxuWqgAuljTM0g9AtNZNr43uhp14z8Pz5vjBJNrNZspFPi1s31Oj0Qo3GUx85J/vCgJC5gYgKK3+WAhignOBIOQDlAfG4ZKel4ePJwfFwkWwgh1Z2kgvQptz0C/E26jH00MiDZzlbs3h3OQ5HvzYs7ehHMZIeYYA5CHd5J1zZ8n/usYimS8HWktWSvgrABd0JHY/Ec+0ArlcACcpB8EK5cME9P6oO37WsBPoUtkKW8VISXUorsO5w1hs+ydvhmUeW1JuR1U87tpQ58xiPb43X8U+dKjUypQh5T7rTwKHoRt0vd/JF8B4w6yoE1fLGg4HGQ03/KbjPA2kFVv2RPoOt1xHYStlNvQKWzMuhNKqDUrrKMhV8CmkAzi4H17I89Bjg4DQQSEj4bB1X0EVIYjezhVhGx2EjECreqqpapUeUxgzbPI4wU9w4hRgitf9XpxRz3BmyP5FQinSSE8jV6QSdSCf4pgHCqGJnCwE66axTTQACEefVnWwC9ANO1hziie5k0es4WVPn7v6gLaeL8P2Ei9ajTMygGbCAJhI4knclO7403KsoGRv1AVq2ROI31KwrqLotfl3AQcQdEKyagAa6XjpAuJObGuX5GVyWsrWcv+ETCfemGbk57Uz4dKRW0hBA5el5dzR6aUZ+WQh10+pXR0nz0qC2njSHg9uSJm6PNFkY3g9pDiZdI82sXt4Bozw1T9ULny1mqtMT3WwDCC406lJlMwvAjtj0uQXUckU0fTnUVEEtb8tso4BarXrJ8lQFyv9gNl5421XngfrGg4FLNc8rZeNIX+F62fi0LbaJy+t3QzZj1DmyGVc0FWe9/5jdHokRkF4sw+iAOhpMcaxbYnt1saop/ldwWcFmmWCn96NgbFQU/P/N5XXdAdbJGk/OZuaM4VO4TNvDS93ryUZOLCg0ckgo+yPpPsCHCNm/hhS8oAsj/snBgrOfVNOS6inb+CvQ6rRUkM7eIBdoFdewKr4Wq+K6EvXZOV3h2ZdtZ8mEYL5eF2Ey3I4l6vFOztJDfQ30r2tmx8sszVlfO5+xVIxyUv5QBUNkbuDsVzLRvy7YQDP/6i0Znn87iJ//AA== \ No newline at end of file diff --git a/driver/camera_linux.go b/driver/camera_linux.go new file mode 100644 index 0000000..014b33d --- /dev/null +++ b/driver/camera_linux.go @@ -0,0 +1,107 @@ +package driver + +// #include +import "C" + +import ( + "github.com/blackjack/webcam" + "github.com/pion/mediadevices/frame" +) + +// Camera implementation using v4l2 +// Reference: https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/videodev.html#videodev +type camera struct { + path string + cam *webcam.Webcam + formats map[webcam.PixelFormat]frame.Format +} + +var _ VideoDriver = &camera{} + +func init() { + // TODO: Probably try to get more cameras + // Get default camera + defaultCam := newCamera("/dev/video0") + + Manager.register(defaultCam) +} + +func newCamera(path string) *camera { + return &camera{ + path: path, + formats: map[webcam.PixelFormat]frame.Format{ + webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV): frame.FormatYUYV, + webcam.PixelFormat(C.V4L2_PIX_FMT_NV12): frame.FormatNV21, + webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG, + }, + } +} + +func (c *camera) Open() error { + cam, err := webcam.Open(c.path) + if err != nil { + return err + } + + c.cam = cam + return nil +} + +func (c *camera) Close() error { + if c.cam == nil { + return nil + } + + return c.cam.StopStreaming() +} + +func (c *camera) Start(spec VideoSpec, cb DataCb) error { + if err := c.cam.StartStreaming(); err != nil { + return err + } + + for { + err := c.cam.WaitForFrame(5) + switch err.(type) { + case nil: + case *webcam.Timeout: + continue + default: + return err + } + + frame, err := c.cam.ReadFrame() + if err != nil { + // TODO: Add a better error handling + return err + } + + if len(frame) == 0 { + continue + } + + cb(frame) + } +} + +func (c *camera) Stop() error { + return c.cam.StopStreaming() +} + +func (c *camera) Info() Info { + return Info{} +} + +func (c *camera) Specs() []VideoSpec { + specs := make([]VideoSpec, 0) + for format := range c.cam.GetSupportedFormats() { + // TODO: get width and height resolutions from camera + specs = append(specs, VideoSpec{ + Width: 640, + Height: 480, + FrameFormat: c.formats[format], + }) + } + + return specs +} diff --git a/driver/const.go b/driver/const.go new file mode 100644 index 0000000..543dfbc --- /dev/null +++ b/driver/const.go @@ -0,0 +1,8 @@ +package driver + +type Kind string + +const ( + Video Kind = "video" + Audio = "audio" +) diff --git a/driver/driver.go b/driver/driver.go new file mode 100644 index 0000000..e1da61d --- /dev/null +++ b/driver/driver.go @@ -0,0 +1,52 @@ +package driver + +import ( + "github.com/pion/mediadevices/frame" +) + +type State uint + +const ( + StateClosed State = iota + StateOpened + StateStarted + StateStopped +) + +type DataCb func(b []byte) + +type Driver interface { + Open() error + Stop() error + Close() error + Info() Info +} + +type Info struct { + Kind Kind +} + +type VideoDriver interface { + Driver + Start(spec VideoSpec, cb DataCb) error + Specs() []VideoSpec +} + +type VideoSpec struct { + Width, Height int + FrameFormat frame.Format +} + +type AudioDriver interface { + Driver + Start(spec AudioSpec, cb DataCb) error + Specs() []AudioSpec +} + +type AudioSpec struct { +} + +type QueryResult struct { + ID string + Driver Driver +} diff --git a/driver/manager.go b/driver/manager.go new file mode 100644 index 0000000..0a309c5 --- /dev/null +++ b/driver/manager.go @@ -0,0 +1,31 @@ +package driver + +import ( + uuid "github.com/satori/go.uuid" +) + +type manager struct { + drivers map[string]Driver +} + +// Manager is a singleton to manage multiple drivers and their states +var Manager = &manager{ + drivers: make(map[string]Driver), +} + +func (m *manager) register(d Driver) { + id := uuid.NewV4() + m.drivers[id.String()] = d +} + +func (m *manager) Query() []QueryResult { + results := make([]QueryResult, 0) + for id, d := range m.drivers { + results = append(results, QueryResult{ + ID: id, + Driver: d, + }) + } + + return results +} diff --git a/examples/simple/main.go b/examples/simple/main.go index 2a5c2f1..1c53e49 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -36,8 +36,8 @@ func main() { panic(err) } - for _, track := range s.GetTracks() { - _, err = peerConnection.AddTrack(track) + for _, tracker := range s.GetTracks() { + _, err = peerConnection.AddTrack(tracker.Track()) if err != nil { panic(err) } diff --git a/go.mod b/go.mod index f774bd6..94059ef 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/blackjack/webcam v0.0.0-20191123110216-08fa32efcb67 github.com/pion/codec v0.0.0-20191205052333-635f762e4bd6 github.com/pion/webrtc/v2 v2.1.12 + github.com/satori/go.uuid v1.2.0 ) diff --git a/go.sum b/go.sum index 125eec8..467571b 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= diff --git a/mediadevices.go b/mediadevices.go index c72e1f6..b478dc0 100644 --- a/mediadevices.go +++ b/mediadevices.go @@ -1,7 +1,7 @@ package mediadevices import ( - "github.com/pion/mediadevices/camera" + "github.com/pion/mediadevices/driver" "github.com/pion/webrtc/v2" ) @@ -20,21 +20,23 @@ type mediaDevices struct { func (m *mediaDevices) GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) { // TODO: It should return media stream based on constraints - c, err := camera.New(camera.Options{ - PC: m.pc, - Codec: webrtc.H264, - Width: 640, - Height: 480, - }) + r := driver.Manager.Query()[0] + err := r.Driver.Open() + if err != nil { + return nil, err + } + d := r.Driver.(driver.VideoDriver) + spec := d.Specs()[0] + + tracker, err := newVideoTrack(m.pc, r.ID, d, spec, webrtc.H264) if err != nil { return nil, err } - s, err := NewMediaStream(c.Track()) + s, err := NewMediaStream(tracker) if err != nil { return nil, err } - go c.Start() return s, nil } diff --git a/mediastream.go b/mediastream.go index 273f36a..04454c5 100644 --- a/mediastream.go +++ b/mediastream.go @@ -7,29 +7,29 @@ import ( ) type MediaStream interface { - GetAudioTracks() []*webrtc.Track - GetVideoTracks() []*webrtc.Track - GetTracks() []*webrtc.Track - AddTrack(t *webrtc.Track) - RemoveTrack(t *webrtc.Track) + GetAudioTracks() []tracker + GetVideoTracks() []tracker + GetTracks() []tracker + AddTrack(t tracker) + RemoveTrack(t tracker) } type mediaStream struct { - tracks map[string]*webrtc.Track - l sync.RWMutex + trackers map[string]tracker + l sync.RWMutex } const rtpCodecTypeDefault webrtc.RTPCodecType = 0 // NewMediaStream creates a MediaStream interface that's defined in // https://w3c.github.io/mediacapture-main/#dom-mediastream -func NewMediaStream(tracks ...*webrtc.Track) (MediaStream, error) { - m := mediaStream{tracks: make(map[string]*webrtc.Track)} +func NewMediaStream(trackers ...tracker) (MediaStream, error) { + m := mediaStream{trackers: make(map[string]tracker)} - for _, track := range tracks { - id := track.ID() - if _, ok := m.tracks[id]; !ok { - m.tracks[id] = track + for _, tracker := range trackers { + id := tracker.Track().ID() + if _, ok := m.trackers[id]; !ok { + m.trackers[id] = tracker } } @@ -37,30 +37,30 @@ func NewMediaStream(tracks ...*webrtc.Track) (MediaStream, error) { } // GetAudioTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getaudiotracks -func (m *mediaStream) GetAudioTracks() []*webrtc.Track { +func (m *mediaStream) GetAudioTracks() []tracker { return m.queryTracks(webrtc.RTPCodecTypeAudio) } // GetVideoTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-getvideotracks -func (m *mediaStream) GetVideoTracks() []*webrtc.Track { +func (m *mediaStream) GetVideoTracks() []tracker { return m.queryTracks(webrtc.RTPCodecTypeVideo) } // GetTracks implements https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks -func (m *mediaStream) GetTracks() []*webrtc.Track { +func (m *mediaStream) GetTracks() []tracker { return m.queryTracks(rtpCodecTypeDefault) } // queryTracks returns all tracks that are the same kind as t. // If t is 0, which is the default, queryTracks will return all the tracks. -func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []*webrtc.Track { +func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []tracker { m.l.RLock() defer m.l.RUnlock() - result := make([]*webrtc.Track, 0) - for _, track := range m.tracks { - if track.Kind() == t || t == rtpCodecTypeDefault { - result = append(result, track) + result := make([]tracker, 0) + for _, tracker := range m.trackers { + if tracker.Track().Kind() == t || t == rtpCodecTypeDefault { + result = append(result, tracker) } } @@ -68,22 +68,22 @@ func (m *mediaStream) queryTracks(t webrtc.RTPCodecType) []*webrtc.Track { } // AddTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-addtrack -func (m *mediaStream) AddTrack(t *webrtc.Track) { +func (m *mediaStream) AddTrack(t tracker) { m.l.Lock() defer m.l.Unlock() - id := t.ID() - if _, ok := m.tracks[id]; ok { + id := t.Track().ID() + if _, ok := m.trackers[id]; ok { return } - m.tracks[id] = t + m.trackers[id] = t } // RemoveTrack implements https://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack -func (m *mediaStream) RemoveTrack(t *webrtc.Track) { +func (m *mediaStream) RemoveTrack(t tracker) { m.l.Lock() defer m.l.Unlock() - delete(m.tracks, t.ID()) + delete(m.trackers, t.Track().ID()) } diff --git a/sampler.go b/sampler.go new file mode 100644 index 0000000..a0e0393 --- /dev/null +++ b/sampler.go @@ -0,0 +1,28 @@ +package mediadevices + +import ( + "time" + + "github.com/pion/webrtc/v2/pkg/media" +) + +type sampler struct { + clockRate float64 + lastTimestamp time.Time +} + +func newSampler(clockRate uint32) *sampler { + return &sampler{ + clockRate: float64(clockRate), + lastTimestamp: time.Now(), + } +} + +func (s *sampler) sample(b []byte) media.Sample { + now := time.Now() + duration := now.Sub(s.lastTimestamp).Seconds() + samples := uint32(s.clockRate * duration) + s.lastTimestamp = now + + return media.Sample{Data: b, Samples: samples} +} diff --git a/track.go b/track.go new file mode 100644 index 0000000..9196f93 --- /dev/null +++ b/track.go @@ -0,0 +1,101 @@ +package mediadevices + +import ( + "fmt" + "math/rand" + + "github.com/pion/codec" + "github.com/pion/codec/h264" + "github.com/pion/mediadevices/driver" + "github.com/pion/mediadevices/frame" + "github.com/pion/webrtc/v2" +) + +type tracker interface { + Track() *webrtc.Track + Stop() +} + +type videoTrack struct { + t *webrtc.Track + s *sampler + d driver.VideoDriver + spec driver.VideoSpec + decoder frame.Decoder + encoder codec.Encoder +} + +func newVideoTrack(pc *webrtc.PeerConnection, id string, d driver.VideoDriver, spec driver.VideoSpec, codecName string) (*videoTrack, error) { + var err error + decoder, err := frame.NewDecoder(spec.FrameFormat) + if err != nil { + return nil, err + } + + var payloadType uint8 + var encoder codec.Encoder + switch codecName { + case webrtc.H264: + payloadType = webrtc.DefaultPayloadTypeH264 + encoder, err = h264.NewEncoder(h264.Options{ + Width: spec.Width, + Height: spec.Height, + Bitrate: 1000000, + MaxFrameRate: 30, + }) + default: + err = fmt.Errorf("%s is currently not supported", codecName) + } + + if err != nil { + return nil, err + } + + track, err := pc.NewTrack(payloadType, rand.Uint32(), "video", id) + if err != nil { + encoder.Close() + return nil, err + } + + vt := videoTrack{ + t: track, + s: newSampler(track.Codec().ClockRate), + d: d, + spec: spec, + decoder: decoder, + encoder: encoder, + } + + go d.Start(spec, vt.dataCb) + return &vt, nil +} + +func (vt *videoTrack) dataCb(b []byte) { + img, err := vt.decoder.Decode(b, vt.spec.Width, vt.spec.Height) + if err != nil { + // TODO: probably do some logging here + return + } + + encoded, err := vt.encoder.Encode(img) + if err != nil { + // TODO: probably do some logging here + return + } + + sample := vt.s.sample(encoded) + err = vt.t.WriteSample(sample) + if err != nil { + // TODO: probably do some logging here + return + } +} + +func (vt *videoTrack) Track() *webrtc.Track { + return vt.t +} + +func (vt *videoTrack) Stop() { + vt.d.Stop() + vt.encoder.Close() +}