diff --git a/README.md b/README.md index 386914f..ee7cb93 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ func main() { * [Face Detection](/examples/facedetection) - Use a machine learning algorithm to detect faces in a camera stream * [RTP Stream](examples/rtp) - Capture camera stream, encode it in H264/VP8/VP9, and send it to a RTP server * [HTTP Broadcast](/examples/http) - Broadcast camera stream through HTTP with MJPEG +* [Archive](/examples/archive) - Archive H264 encoded video stream from a camera ## Available Media Inputs diff --git a/examples/archive/README.md b/examples/archive/README.md new file mode 100644 index 0000000..54832b3 --- /dev/null +++ b/examples/archive/README.md @@ -0,0 +1,36 @@ +## Instructions + +### Install required codecs + +In this example, we'll be using x264 as our video codec. Therefore, we need to make sure that these codecs are installed within our system. + +Installation steps: + +* [x264](https://github.com/pion/mediadevices#x264) + +### Download archive examplee + +``` +git clone https://github.com/pion/mediadevices.git +``` + +### Run archive example + +Run `cd mediadevices/examples/archive && go build && ./archive recorded.h264` + +### Playback recorded video + +Install GStreamer and run: +``` +gst-launch-1.0 playbin uri=file://${PWD}/recorded.h264 +``` + +Or run VLC media plyer: +``` +vlc recorded.h264 +``` + +A video should start playing in your GStreamer or VLC window. + +Congrats, you have used pion-MediaDevices! Now start building something cool + diff --git a/examples/archive/archive b/examples/archive/archive new file mode 100755 index 0000000..ea99e8e Binary files /dev/null and b/examples/archive/archive differ diff --git a/examples/archive/main.go b/examples/archive/main.go new file mode 100644 index 0000000..152c0cf --- /dev/null +++ b/examples/archive/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "image" + "io" + "os" + "os/signal" + "syscall" + + "github.com/pion/mediadevices" + "github.com/pion/mediadevices/pkg/codec/x264" // This is required to use H264 video encoder + _ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter + "github.com/pion/mediadevices/pkg/frame" + "github.com/pion/mediadevices/pkg/io/video" + "github.com/pion/mediadevices/pkg/prop" +) + +func must(err error) { + if err != nil { + panic(err) + } +} + +func main() { + if len(os.Args) != 2 { + fmt.Printf("usage: %s \n", os.Args[0]) + return + } + dest := os.Args[1] + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT) + + x264Params, err := x264.NewParams() + must(err) + x264Params.Preset = x264.PresetMedium + x264Params.BitRate = 1_000_000 // 1mbps + + codecSelector := mediadevices.NewCodecSelector( + mediadevices.WithVideoEncoders(&x264Params), + ) + + mediaStream, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ + Video: func(c *mediadevices.MediaTrackConstraints) { + c.FrameFormat = prop.FrameFormat(frame.FormatYUY2) + c.Width = prop.Int(640) + c.Height = prop.Int(480) + }, + Codec: codecSelector, + }) + must(err) + + videoTrack := mediaStream.GetVideoTracks()[0].(*mediadevices.VideoTrack) + defer videoTrack.Close() + + videoTrack.Transform(video.TransformFunc(func(r video.Reader) video.Reader { + return video.ReaderFunc(func() (img image.Image, release func(), err error) { + // we send io.EOF signal to the encoder reader to stop reading. Therefore, io.Copy + // will finish its execution and the program will finish + select { + case <-sigs: + return nil, func() {}, io.EOF + default: + } + + return r.Read() + }) + })) + + reader, err := videoTrack.NewEncodedReader(x264Params.RTPCodec().Name) + must(err) + defer reader.Close() + + out, err := os.Create(dest) + must(err) + + fmt.Println("Recording... Press Ctrl+c to stop") + _, err = io.Copy(out, reader) + must(err) + fmt.Println("Your video has been recorded to", dest) +} diff --git a/ioreader.go b/ioreader.go new file mode 100644 index 0000000..6fc526c --- /dev/null +++ b/ioreader.go @@ -0,0 +1,14 @@ +package mediadevices + +type encodedReadCloserImpl struct { + readFn func([]byte) (int, error) + closeFn func() error +} + +func (r *encodedReadCloserImpl) Read(b []byte) (int, error) { + return r.readFn(b) +} + +func (r *encodedReadCloserImpl) Close() error { + return r.closeFn() +} diff --git a/mediastream_test.go b/mediastream_test.go index c4f2acd..f9d14dd 100644 --- a/mediastream_test.go +++ b/mediastream_test.go @@ -1,6 +1,7 @@ package mediadevices import ( + "io" "testing" "github.com/pion/webrtc/v2" @@ -37,6 +38,10 @@ func (track *mockMediaStreamTrack) NewRTPReader(codecName string, mtu int) (RTPR return nil, nil } +func (track *mockMediaStreamTrack) NewEncodedReader(codecName string) (io.ReadCloser, error) { + return nil, nil +} + func TestMediaStreamFilters(t *testing.T) { audioTracks := []Track{ &mockMediaStreamTrack{AudioInput}, diff --git a/track.go b/track.go index 1ecda98..a0b6276 100644 --- a/track.go +++ b/track.go @@ -3,6 +3,7 @@ package mediadevices import ( "errors" "image" + "io" "math/rand" "sync" @@ -57,6 +58,8 @@ type Track interface { // NewRTPReader creates a new reader from the source. The reader will encode the source, and packetize // the encoded data in RTP format with given mtu size. NewRTPReader(codecName string, mtu int) (RTPReadCloser, error) + // NewEncodedReader creates a new Go standard io.ReadCloser that reads the encoded data in codecName format + NewEncodedReader(codecName string) (io.ReadCloser, error) } type baseTrack struct { @@ -182,6 +185,31 @@ func (track *baseTrack) unbind(pc *webrtc.PeerConnection) error { return nil } +func (track *baseTrack) newEncodedReader(reader codec.ReadCloser) (io.ReadCloser, error) { + var encoded []byte + release := func() {} + return &encodedReadCloserImpl{ + readFn: func(b []byte) (int, error) { + var err error + + if len(encoded) == 0 { + release() + encoded, release, err = reader.Read() + if err != nil { + reader.Close() + track.onError(err) + return 0, err + } + } + + n := copy(b, encoded) + encoded = encoded[n:] + return n, nil + }, + closeFn: reader.Close, + }, nil +} + func newTrackFromDriver(d driver.Driver, constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { if err := d.Open(); err != nil { return nil, err @@ -298,6 +326,21 @@ func (track *VideoTrack) NewRTPReader(codecName string, mtu int) (RTPReadCloser, }, nil } +func (track *VideoTrack) NewEncodedReader(codecName string) (io.ReadCloser, error) { + reader := track.NewReader(false) + inputProp, err := detectCurrentVideoProp(track.Broadcaster) + if err != nil { + return nil, err + } + + encodedReader, _, err := track.selector.selectVideoCodecByNames(reader, inputProp, codecName) + if err != nil { + return nil, err + } + + return track.newEncodedReader(encodedReader) +} + // AudioTrack is a specific track type that contains audio source which allows multiple readers to access, and // manipulate. type AudioTrack struct { @@ -402,3 +445,18 @@ func (track *AudioTrack) NewRTPReader(codecName string, mtu int) (RTPReadCloser, closeFn: encodedReader.Close, }, nil } + +func (track *AudioTrack) NewEncodedReader(codecName string) (io.ReadCloser, error) { + reader := track.NewReader(false) + inputProp, err := detectCurrentAudioProp(track.Broadcaster) + if err != nil { + return nil, err + } + + encodedReader, _, err := track.selector.selectAudioCodecByNames(reader, inputProp, codecName) + if err != nil { + return nil, err + } + + return track.newEncodedReader(encodedReader) +}