mirror of
https://github.com/pion/mediadevices.git
synced 2025-09-27 21:02:17 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
716da16e4a | ||
![]() |
1550a68003 | ||
![]() |
d65170dfe3 | ||
![]() |
4057524bf0 |
@@ -79,6 +79,7 @@ func main() {
|
|||||||
* [Face Detection](/examples/facedetection) - Use a machine learning algorithm to detect faces in a camera stream
|
* [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
|
* [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
|
* [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
|
## Available Media Inputs
|
||||||
|
|
||||||
|
36
examples/archive/README.md
Normal file
36
examples/archive/README.md
Normal file
@@ -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
|
||||||
|
|
BIN
examples/archive/archive
Executable file
BIN
examples/archive/archive
Executable file
Binary file not shown.
82
examples/archive/main.go
Normal file
82
examples/archive/main.go
Normal file
@@ -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 <path/to/file.h264>\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)
|
||||||
|
}
|
14
ioreader.go
Normal file
14
ioreader.go
Normal file
@@ -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()
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package mediadevices
|
package mediadevices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pion/webrtc/v2"
|
"github.com/pion/webrtc/v2"
|
||||||
@@ -37,6 +38,10 @@ func (track *mockMediaStreamTrack) NewRTPReader(codecName string, mtu int) (RTPR
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (track *mockMediaStreamTrack) NewEncodedReader(codecName string) (io.ReadCloser, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestMediaStreamFilters(t *testing.T) {
|
func TestMediaStreamFilters(t *testing.T) {
|
||||||
audioTracks := []Track{
|
audioTracks := []Track{
|
||||||
&mockMediaStreamTrack{AudioInput},
|
&mockMediaStreamTrack{AudioInput},
|
||||||
|
@@ -237,10 +237,19 @@ func (e *encoder) Read() ([]byte, func(), error) {
|
|||||||
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
e.raw.d_w, e.raw.d_h = C.uint(width), C.uint(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duration := t - e.tLastFrame
|
||||||
|
// VPX doesn't allow 0 duration. If 0 is given, vpx_codec_encode will fail with VPX_CODEC_INVALID_PARAM.
|
||||||
|
// 0 duration is possible because mediadevices first gets the frame meta data by reading from the source,
|
||||||
|
// and consequently the codec will read the first frame from the buffer. This means the first frame won't
|
||||||
|
// have a pause to the second frame, which means if the delay is <1 ms (vpx duration resolution), duration
|
||||||
|
// is going to be 0.
|
||||||
|
if duration == 0 {
|
||||||
|
duration = 1
|
||||||
|
}
|
||||||
var flags int
|
var flags int
|
||||||
if ec := C.encode_wrapper(
|
if ec := C.encode_wrapper(
|
||||||
e.codec, e.raw,
|
e.codec, e.raw,
|
||||||
C.long(t-e.tStart), C.ulong(t-e.tLastFrame), C.long(flags), C.ulong(e.deadline),
|
C.long(t-e.tStart), C.ulong(duration), C.long(flags), C.ulong(e.deadline),
|
||||||
(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]),
|
(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]),
|
||||||
); ec != C.VPX_CODEC_OK {
|
); ec != C.VPX_CODEC_OK {
|
||||||
return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec)
|
return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec)
|
||||||
|
60
track.go
60
track.go
@@ -3,6 +3,7 @@ package mediadevices
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ type Track interface {
|
|||||||
// NewRTPReader creates a new reader from the source. The reader will encode the source, and packetize
|
// 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.
|
// the encoded data in RTP format with given mtu size.
|
||||||
NewRTPReader(codecName string, mtu int) (RTPReadCloser, error)
|
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 {
|
type baseTrack struct {
|
||||||
@@ -182,6 +185,31 @@ func (track *baseTrack) unbind(pc *webrtc.PeerConnection) error {
|
|||||||
return nil
|
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) {
|
func newTrackFromDriver(d driver.Driver, constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) {
|
||||||
if err := d.Open(); err != nil {
|
if err := d.Open(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -298,6 +326,21 @@ func (track *VideoTrack) NewRTPReader(codecName string, mtu int) (RTPReadCloser,
|
|||||||
}, nil
|
}, 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
|
// AudioTrack is a specific track type that contains audio source which allows multiple readers to access, and
|
||||||
// manipulate.
|
// manipulate.
|
||||||
type AudioTrack struct {
|
type AudioTrack struct {
|
||||||
@@ -380,7 +423,7 @@ func (track *AudioTrack) NewRTPReader(codecName string, mtu int) (RTPReadCloser,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sample := newVideoSampler(selectedCodec.ClockRate)
|
sample := newAudioSampler(selectedCodec.ClockRate, inputProp.Latency)
|
||||||
|
|
||||||
// FIXME: not sure the best way to get unique ssrc. We probably should have a global keeper that can generate a random ID and does book keeping?
|
// FIXME: not sure the best way to get unique ssrc. We probably should have a global keeper that can generate a random ID and does book keeping?
|
||||||
packetizer := rtp.NewPacketizer(mtu, selectedCodec.PayloadType, rand.Uint32(), selectedCodec.Payloader, rtp.NewRandomSequencer(), selectedCodec.ClockRate)
|
packetizer := rtp.NewPacketizer(mtu, selectedCodec.PayloadType, rand.Uint32(), selectedCodec.Payloader, rtp.NewRandomSequencer(), selectedCodec.ClockRate)
|
||||||
@@ -402,3 +445,18 @@ func (track *AudioTrack) NewRTPReader(codecName string, mtu int) (RTPReadCloser,
|
|||||||
closeFn: encodedReader.Close,
|
closeFn: encodedReader.Close,
|
||||||
}, nil
|
}, 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)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user