mirror of
				https://github.com/pion/mediadevices.git
				synced 2025-10-31 11:56:28 +08:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c4e7159480 | ||
|   | 7a4ca55b41 | ||
|   | 1081f12587 | 
							
								
								
									
										22
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -12,13 +12,13 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         go: [ '1.15', '1.14' ] | ||||
|         go: [ '1.14', '1.13' ] | ||||
|     name: Linux Go ${{ matrix.go }} | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v2 | ||||
|         uses: actions/setup-go@v1 | ||||
|         with: | ||||
|           go-version: ${{ matrix.go }} | ||||
|       - name: Install dependencies | ||||
| @@ -30,17 +30,15 @@ jobs: | ||||
|             libvpx-dev \ | ||||
|             libx264-dev | ||||
|       - name: go vet | ||||
|         run: go vet $(go list ./... | grep -v mmal) | ||||
|         run: go vet ./... | ||||
|       - name: go build | ||||
|         run: go build $(go list ./... | grep -v mmal) | ||||
|         run: go build ./... | ||||
|       - name: go build without CGO | ||||
|         run: go build . pkg/... | ||||
|         env: | ||||
|           CGO_ENABLED: 0 | ||||
|       - name: go test | ||||
|         run: go test -v -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v mmal) | ||||
|       - uses: codecov/codecov-action@v1 | ||||
|         if: matrix.go == '1.15' | ||||
|         run: go test ./... -v -race | ||||
|       - name: go test without CGO | ||||
|         run: go test . pkg/... -v | ||||
|         env: | ||||
| @@ -49,13 +47,13 @@ jobs: | ||||
|     runs-on: macos-latest | ||||
|     strategy: | ||||
|       matrix: | ||||
|         go: [ '1.15', '1.14' ] | ||||
|         go: [ '1.14', '1.13' ] | ||||
|     name: Darwin Go ${{ matrix.go }} | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|       - name: Setup Go | ||||
|         uses: actions/setup-go@v2 | ||||
|         uses: actions/setup-go@v1 | ||||
|         with: | ||||
|           go-version: ${{ matrix.go }} | ||||
|       - name: Install dependencies | ||||
| @@ -66,15 +64,15 @@ jobs: | ||||
|             libvpx \ | ||||
|             x264 | ||||
|       - name: go vet | ||||
|         run: go vet $(go list ./... | grep -v mmal) | ||||
|         run: go vet ./... | ||||
|       - name: go build | ||||
|         run: go build $(go list ./... | grep -v mmal) | ||||
|         run: go build ./... | ||||
|       - name: go build without CGO | ||||
|         run: go build . pkg/... | ||||
|         env: | ||||
|           CGO_ENABLED: 0 | ||||
|       - name: go test | ||||
|         run: go test -v -race $(go list ./... | grep -v mmal) | ||||
|         run: go test ./... -v -race | ||||
|       - name: go test without CGO | ||||
|         run: go test . pkg/... -v | ||||
|         env: | ||||
|   | ||||
							
								
								
									
										1
									
								
								CODEOWNERS
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								CODEOWNERS
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| * @lherman-cs @at-wat | ||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,17 +1,6 @@ | ||||
| <h1 align="center"> | ||||
|   <br> | ||||
|   Pion MediaDevices | ||||
|   <br> | ||||
| </h1> | ||||
| <h4 align="center">Go implementation of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices">MediaDevices</a> API</h4> | ||||
| <p align="center"> | ||||
|   <a href="https://pion.ly/slack"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a> | ||||
|   <a href="https://github.com/pion/mediadevices/actions"><img src="https://github.com/pion/mediadevices/workflows/CI/badge.svg?branch=master" alt="Build status"></a>  | ||||
|   <a href="https://pkg.go.dev/github.com/pion/mediadevices"><img src="https://godoc.org/github.com/pion/mediadevices?status.svg" alt="GoDoc"></a> | ||||
|   <a href="https://codecov.io/gh/pion/mediadevices"><img src="https://codecov.io/gh/pion/mediadevices/branch/master/graph/badge.svg" alt="Coverage Status"></a> | ||||
|   <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a> | ||||
| </p> | ||||
| <br> | ||||
| # mediadevices | ||||
|  | ||||
| Go implementation of the [MediaDevices](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices) API. | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										117
									
								
								codec.go
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								codec.go
									
									
									
									
									
								
							| @@ -1,117 +0,0 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // CodecSelector is a container of video and audio encoder builders, which later will be used | ||||
| // for codec matching. | ||||
| type CodecSelector struct { | ||||
| 	videoEncoders []codec.VideoEncoderBuilder | ||||
| 	audioEncoders []codec.AudioEncoderBuilder | ||||
| } | ||||
|  | ||||
| // CodecSelectorOption is a type for specifying CodecSelector options | ||||
| type CodecSelectorOption func(*CodecSelector) | ||||
|  | ||||
| // WithVideoEncoders replace current video codecs with listed encoders | ||||
| func WithVideoEncoders(encoders ...codec.VideoEncoderBuilder) CodecSelectorOption { | ||||
| 	return func(t *CodecSelector) { | ||||
| 		t.videoEncoders = encoders | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WithVideoEncoders replace current audio codecs with listed encoders | ||||
| func WithAudioEncoders(encoders ...codec.AudioEncoderBuilder) CodecSelectorOption { | ||||
| 	return func(t *CodecSelector) { | ||||
| 		t.audioEncoders = encoders | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewCodecSelector constructs CodecSelector with given variadic options | ||||
| func NewCodecSelector(opts ...CodecSelectorOption) *CodecSelector { | ||||
| 	var track CodecSelector | ||||
|  | ||||
| 	for _, opt := range opts { | ||||
| 		opt(&track) | ||||
| 	} | ||||
|  | ||||
| 	return &track | ||||
| } | ||||
|  | ||||
| // Populate lets the webrtc engine be aware of supported codecs that are contained in CodecSelector | ||||
| func (selector *CodecSelector) Populate(setting *webrtc.MediaEngine) { | ||||
| 	for _, encoder := range selector.videoEncoders { | ||||
| 		setting.RegisterCodec(encoder.RTPCodec().RTPCodec) | ||||
| 	} | ||||
|  | ||||
| 	for _, encoder := range selector.audioEncoders { | ||||
| 		setting.RegisterCodec(encoder.RTPCodec().RTPCodec) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (selector *CodecSelector) selectVideoCodec(wantCodecs []*webrtc.RTPCodec, reader video.Reader, inputProp prop.Media) (codec.ReadCloser, *codec.RTPCodec, error) { | ||||
| 	var selectedEncoder codec.VideoEncoderBuilder | ||||
| 	var encodedReader codec.ReadCloser | ||||
| 	var errReasons []string | ||||
| 	var err error | ||||
|  | ||||
| outer: | ||||
| 	for _, wantCodec := range wantCodecs { | ||||
| 		name := wantCodec.Name | ||||
| 		for _, encoder := range selector.videoEncoders { | ||||
| 			if encoder.RTPCodec().Name == name { | ||||
| 				encodedReader, err = encoder.BuildVideoEncoder(reader, inputProp) | ||||
| 				if err == nil { | ||||
| 					selectedEncoder = encoder | ||||
| 					break outer | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().Name, err)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if selectedEncoder == nil { | ||||
| 		return nil, nil, errors.New(strings.Join(errReasons, "\n\n")) | ||||
| 	} | ||||
|  | ||||
| 	return encodedReader, selectedEncoder.RTPCodec(), nil | ||||
| } | ||||
|  | ||||
| func (selector *CodecSelector) selectAudioCodec(wantCodecs []*webrtc.RTPCodec, reader audio.Reader, inputProp prop.Media) (codec.ReadCloser, *codec.RTPCodec, error) { | ||||
| 	var selectedEncoder codec.AudioEncoderBuilder | ||||
| 	var encodedReader codec.ReadCloser | ||||
| 	var errReasons []string | ||||
| 	var err error | ||||
|  | ||||
| outer: | ||||
| 	for _, wantCodec := range wantCodecs { | ||||
| 		name := wantCodec.Name | ||||
| 		for _, encoder := range selector.audioEncoders { | ||||
| 			if encoder.RTPCodec().Name == name { | ||||
| 				encodedReader, err = encoder.BuildAudioEncoder(reader, inputProp) | ||||
| 				if err == nil { | ||||
| 					selectedEncoder = encoder | ||||
| 					break outer | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			errReasons = append(errReasons, fmt.Sprintf("%s: %s", encoder.RTPCodec().Name, err)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if selectedEncoder == nil { | ||||
| 		return nil, nil, errors.New(strings.Join(errReasons, "\n\n")) | ||||
| 	} | ||||
|  | ||||
| 	return encodedReader, selectedEncoder.RTPCodec(), nil | ||||
| } | ||||
							
								
								
									
										10
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -1,10 +0,0 @@ | ||||
| coverage: | ||||
|   status: | ||||
|     project: | ||||
|       default: | ||||
|         # Allow decreasing 2% of total coverage to avoid noise. | ||||
|         threshold: 2% | ||||
|     patch: off | ||||
|  | ||||
| ignore: | ||||
|   - "examples/*" | ||||
| @@ -2,8 +2,8 @@ module github.com/pion/mediadevices/examples | ||||
|  | ||||
| go 1.14 | ||||
|  | ||||
| replace github.com/pion/mediadevices => ../ | ||||
|  | ||||
| // Please don't commit require entries of examples. | ||||
| // `git checkout master examples/go.mod` to revert this file. | ||||
| require github.com/pion/mediadevices v0.0.0 | ||||
|  | ||||
| replace github.com/pion/mediadevices v0.0.0 => ../ | ||||
| require github.com/pion/mediadevices v0.0.0-00010101000000-000000000000 | ||||
|   | ||||
							
								
								
									
										17
									
								
								examples/simple/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/simple/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| ## Instructions | ||||
|  | ||||
| ### Download the example | ||||
|  | ||||
| ``` | ||||
| go get github.com/pion/mediadevices/examples/simple | ||||
| ``` | ||||
|  | ||||
| ### Run the sample | ||||
|  | ||||
| ``` | ||||
| simple | ||||
| ``` | ||||
|  | ||||
| ### View yourself in the browser | ||||
|  | ||||
| Open your browser and go to "http://localhost:1313" | ||||
| @@ -1,9 +1,6 @@ | ||||
| // This is an example of using mediadevices to broadcast your camera through http. | ||||
| // The example doesn't aim to be performant, but rather it strives to be simple. | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"image/jpeg" | ||||
| 	"io" | ||||
| @@ -21,12 +18,6 @@ import ( | ||||
| 	_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter | ||||
| ) | ||||
| 
 | ||||
| func must(err error) { | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ | ||||
| 		Video: func(constraint *mediadevices.MediaTrackConstraints) { | ||||
| @@ -34,14 +25,16 @@ func main() { | ||||
| 			constraint.Height = prop.Int(400) | ||||
| 		}, | ||||
| 	}) | ||||
| 	must(err) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 
 | ||||
| 	t := s.GetVideoTracks()[0] | ||||
| 	defer t.Stop() | ||||
| 	videoTrack := t.(*mediadevices.VideoTrack) | ||||
| 
 | ||||
| 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		var buf bytes.Buffer | ||||
| 		videoReader := videoTrack.NewReader(false) | ||||
| 		videoReader := videoTrack.NewReader() | ||||
| 		mimeWriter := multipart.NewWriter(w) | ||||
| 
 | ||||
| 		contentType := fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary()) | ||||
| @@ -51,24 +44,23 @@ func main() { | ||||
| 		partHeader.Add("Content-Type", "image/jpeg") | ||||
| 
 | ||||
| 		for { | ||||
| 			frame, release, err := videoReader.Read() | ||||
| 			if err == io.EOF { | ||||
| 				return | ||||
| 			frame, err := videoReader.Read() | ||||
| 			if err != nil { | ||||
| 				if err == io.EOF { | ||||
| 					return | ||||
| 				} | ||||
| 				panic(err) | ||||
| 			} | ||||
| 			must(err) | ||||
| 
 | ||||
| 			err = jpeg.Encode(&buf, frame, nil) | ||||
| 			// Since we're done with img, we need to release img so that that the original owner can reuse | ||||
| 			// this memory. | ||||
| 			release() | ||||
| 			must(err) | ||||
| 
 | ||||
| 			partWriter, err := mimeWriter.CreatePart(partHeader) | ||||
| 			must(err) | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 
 | ||||
| 			_, err = partWriter.Write(buf.Bytes()) | ||||
| 			buf.Reset() | ||||
| 			must(err) | ||||
| 			err = jpeg.Encode(partWriter, frame, nil) | ||||
| 			if err != nil { | ||||
| 				panic(err) | ||||
| 			} | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| @@ -1,29 +0,0 @@ | ||||
| ## Instructions | ||||
|  | ||||
| ### Download gstreamer-send | ||||
|  | ||||
| ``` | ||||
| go get github.com/pion/mediadevices/examples/webrtc | ||||
| ``` | ||||
|  | ||||
| ### Open example page | ||||
|  | ||||
| [jsfiddle.net](https://jsfiddle.net/gh/get/library/pure/pion/mediadevices/tree/master/examples/internal/jsfiddle/audio-and-video) you should see two text-areas and a 'Start Session' button | ||||
|  | ||||
| ### Run webrtc with your browsers SessionDescription as stdin | ||||
|  | ||||
| In the jsfiddle the top textarea is your browser, copy that and: | ||||
|  | ||||
| #### Linux | ||||
|  | ||||
| Run `echo $BROWSER_SDP | webrtc` | ||||
|  | ||||
| ### Input webrtc's SessionDescription into your browser | ||||
|  | ||||
| Copy the text that `webrtc` just emitted and copy into second text area | ||||
|  | ||||
| ### Hit 'Start Session' in jsfiddle, enjoy your video! | ||||
|  | ||||
| A video should start playing in your browser above the input boxes, and will continue playing until you close the application. | ||||
|  | ||||
| Congrats, you have used pion-WebRTC! Now start building something cool | ||||
| @@ -1,136 +0,0 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/pion/mediadevices" | ||||
| 	"github.com/pion/mediadevices/examples/internal/signal" | ||||
| 	"github.com/pion/mediadevices/pkg/frame" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
|  | ||||
| 	// If you don't like x264, you can also use vpx by importing as below | ||||
| 	// "github.com/pion/mediadevices/pkg/codec/vpx" // This is required to use VP8/VP9 video encoder | ||||
| 	// or you can also use openh264 for alternative h264 implementation | ||||
| 	// "github.com/pion/mediadevices/pkg/codec/openh264" | ||||
| 	// or if you use a raspberry pi like, you can use mmal for using its hardware encoder | ||||
| 	// "github.com/pion/mediadevices/pkg/codec/mmal" | ||||
| 	"github.com/pion/mediadevices/pkg/codec/opus" // This is required to use opus audio encoder | ||||
| 	"github.com/pion/mediadevices/pkg/codec/x264" // This is required to use h264 video encoder | ||||
|  | ||||
| 	// Note: If you don't have a camera or microphone or your adapters are not supported, | ||||
| 	//       you can always swap your adapters with our dummy adapters below. | ||||
| 	// _ "github.com/pion/mediadevices/pkg/driver/videotest" | ||||
| 	// _ "github.com/pion/mediadevices/pkg/driver/audiotest" | ||||
| 	_ "github.com/pion/mediadevices/pkg/driver/camera"     // This is required to register camera adapter | ||||
| 	_ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	videoCodecName = webrtc.VP8 | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	config := webrtc.Configuration{ | ||||
| 		ICEServers: []webrtc.ICEServer{ | ||||
| 			{ | ||||
| 				URLs: []string{"stun:stun.l.google.com:19302"}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	// Wait for the offer to be pasted | ||||
| 	offer := webrtc.SessionDescription{} | ||||
| 	signal.Decode(signal.MustReadStdin(), &offer) | ||||
|  | ||||
| 	// Create a new RTCPeerConnection | ||||
| 	x264Params, err := x264.NewParams() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	x264Params.BitRate = 500_000 // 500kbps | ||||
|  | ||||
| 	opusParams, err := opus.NewParams() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	codecSelector := mediadevices.NewCodecSelector( | ||||
| 		mediadevices.WithVideoEncoders(&x264Params), | ||||
| 		mediadevices.WithAudioEncoders(&opusParams), | ||||
| 	) | ||||
|  | ||||
| 	mediaEngine := webrtc.MediaEngine{} | ||||
| 	codecSelector.Populate(&mediaEngine) | ||||
| 	if err := mediaEngine.PopulateFromSDP(offer); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine)) | ||||
| 	peerConnection, err := api.NewPeerConnection(config) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	// Set the handler for ICE connection state | ||||
| 	// This will notify you when the peer has connected/disconnected | ||||
| 	peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { | ||||
| 		fmt.Printf("Connection State has changed %s \n", connectionState.String()) | ||||
| 	}) | ||||
|  | ||||
| 	s, 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) | ||||
| 		}, | ||||
| 		Audio: func(c *mediadevices.MediaTrackConstraints) { | ||||
| 		}, | ||||
| 		Codec: codecSelector, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	for _, tracker := range s.GetTracks() { | ||||
| 		tracker.OnEnded(func(err error) { | ||||
| 			fmt.Printf("Track (ID: %s) ended with error: %v\n", | ||||
| 				tracker.ID(), err) | ||||
| 		}) | ||||
|  | ||||
| 		// In Pion/webrtc v3, bind will be called automatically after SDP negotiation | ||||
| 		webrtcTrack, err := tracker.Bind(peerConnection) | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
|  | ||||
| 		_, err = peerConnection.AddTransceiverFromTrack(webrtcTrack, | ||||
| 			webrtc.RtpTransceiverInit{ | ||||
| 				Direction: webrtc.RTPTransceiverDirectionSendonly, | ||||
| 			}, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Set the remote SessionDescription | ||||
| 	err = peerConnection.SetRemoteDescription(offer) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	// Create an answer | ||||
| 	answer, err := peerConnection.CreateAnswer(nil) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	// Sets the LocalDescription, and starts our UDP listeners | ||||
| 	err = peerConnection.SetLocalDescription(answer) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	// Output the answer in base64 so we can paste it in browser | ||||
| 	fmt.Println(signal.Encode(answer)) | ||||
| 	select {} | ||||
| } | ||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.mod
									
									
									
									
									
								
							| @@ -4,11 +4,10 @@ go 1.13 | ||||
|  | ||||
| require ( | ||||
| 	github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 | ||||
| 	github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93 | ||||
| 	github.com/lherman-cs/opus v0.0.2 | ||||
| 	github.com/pion/logging v0.2.2 | ||||
| 	github.com/pion/webrtc/v2 v2.2.26 | ||||
| 	github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214 | ||||
| 	github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4 | ||||
| 	github.com/pion/webrtc/v2 v2.2.23 | ||||
| 	github.com/satori/go.uuid v1.2.0 | ||||
| 	golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 | ||||
| 	golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f | ||||
| 	golang.org/x/image v0.0.0-20200801110659-972c09e46d76 | ||||
| 	golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										38
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								go.sum
									
									
									
									
									
								
							| @@ -15,15 +15,15 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= | ||||
| github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93 h1:gDcaH96SZ7q1JU6hj0tSv8FiuqadFERU17lLxhphLa8= | ||||
| github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= | ||||
| github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214 h1:2xVJKIumEUWeV3vczQwn61SHjNZ94Bwk+4CTjmcePxk= | ||||
| github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= | ||||
| github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/lherman-cs/opus v0.0.2 h1:fE9Du3NKXDBztqvoTd6P2y9eJ9vgIHahGK8yQostnhA= | ||||
| github.com/lherman-cs/opus v0.0.2/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps= | ||||
| github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4 h1:2ydMA2KbxRkYmIw3R8Me8dn90bejxBR4MKYXJ5THK3I= | ||||
| github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps= | ||||
| github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= | ||||
| github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= | ||||
| github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= | ||||
| @@ -33,8 +33,9 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= | ||||
| github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= | ||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= | ||||
| github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= | ||||
| github.com/pion/datachannel v1.4.19 h1:IcOmm5fdDzJVCMgFYDCMtFC+lrjG78KcMYXH+gOo6ys= | ||||
| github.com/pion/datachannel v1.4.19/go.mod h1:JzKF/zzeWgkOYwQ+KFb8JzbrUt8s63um+Qunu8VqTyw= | ||||
| github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA= | ||||
| github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U= | ||||
| github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E= | ||||
| github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I= | ||||
| @@ -53,8 +54,8 @@ github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA= | ||||
| github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I= | ||||
| github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk= | ||||
| github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI= | ||||
| github.com/pion/sctp v1.7.10 h1:o3p3/hZB5Cx12RMGyWmItevJtZ6o2cpuxaw6GOS4x+8= | ||||
| github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= | ||||
| github.com/pion/sctp v1.7.8 h1:tEWel2BKXLZitU+LxY3GDeQXoKeTafYasiu/X+XBKNM= | ||||
| github.com/pion/sctp v1.7.8/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= | ||||
| github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI= | ||||
| github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= | ||||
| github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= | ||||
| @@ -62,7 +63,9 @@ github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkc | ||||
| github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= | ||||
| github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= | ||||
| github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= | ||||
| github.com/pion/transport v0.8.10 h1:lTiobMEw2PG6BH/mgIVqTV2mBp/mPT+IJLaN8ZxgdHk= | ||||
| github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= | ||||
| github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80= | ||||
| github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= | ||||
| github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM= | ||||
| github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= | ||||
| @@ -70,8 +73,8 @@ github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk= | ||||
| github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= | ||||
| github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= | ||||
| github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= | ||||
| github.com/pion/webrtc/v2 v2.2.26 h1:01hWE26pL3LgqfxvQ1fr6O4ZtyRFFJmQEZK39pHWfFc= | ||||
| github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc= | ||||
| github.com/pion/webrtc/v2 v2.2.23 h1:rZdOC95fwUCoQFVjHooPAayx/vhs3SLHFz8J/iRkAuk= | ||||
| github.com/pion/webrtc/v2 v2.2.23/go.mod h1:1lN/3EcATkQxc7GJSQbISCGC2l64Xu2VSLpwEG3c/tM= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| @@ -81,23 +84,29 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh | ||||
| github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= | ||||
| 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= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= | ||||
| github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= | ||||
| github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20200602180216-279210d13fed h1:g4KENRiCMEx58Q7/ecwfT0N2o8z35Fnbsjig/Alf2T4= | ||||
| golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= | ||||
| golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= | ||||
| golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||
| golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= | ||||
| golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= | ||||
| golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | ||||
| golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= | ||||
| golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||
| golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= | ||||
| golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | ||||
| @@ -108,9 +117,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h | ||||
| golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= | ||||
| golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= | ||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE= | ||||
| golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| package logging | ||||
|  | ||||
| import ( | ||||
| 	"github.com/pion/logging" | ||||
| ) | ||||
|  | ||||
| var loggerFactory = logging.NewDefaultLoggerFactory() | ||||
|  | ||||
| func NewLogger(scope string) logging.LeveledLogger { | ||||
| 	return loggerFactory.NewLogger(scope) | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"github.com/pion/mediadevices/internal/logging" | ||||
| ) | ||||
|  | ||||
| var logger = logging.NewLogger("mediadevices") | ||||
| @@ -7,7 +7,7 @@ type MediaDeviceType int | ||||
|  | ||||
| // MediaDeviceType definitions. | ||||
| const ( | ||||
| 	VideoInput MediaDeviceType = iota + 1 | ||||
| 	VideoInput MediaDeviceType = iota | ||||
| 	AudioInput | ||||
| 	AudioOutput | ||||
| ) | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package mediadevices | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/driver" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| @@ -15,29 +14,29 @@ var errNotFound = fmt.Errorf("failed to find the best driver that fits the const | ||||
| // of a display or portion thereof (such as a window) as a MediaStream. | ||||
| // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia | ||||
| func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) { | ||||
| 	trackers := make([]Track, 0) | ||||
| 	tracks := make([]Track, 0) | ||||
|  | ||||
| 	cleanTrackers := func() { | ||||
| 		for _, t := range trackers { | ||||
| 			t.Close() | ||||
| 	cleanTracks := func() { | ||||
| 		for _, t := range tracks { | ||||
| 			t.Stop() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var videoConstraints MediaTrackConstraints | ||||
| 	if constraints.Video != nil { | ||||
| 		constraints.Video(&videoConstraints) | ||||
| 		tracker, err := selectScreen(videoConstraints, constraints.Codec) | ||||
| 		var p MediaTrackConstraints | ||||
| 		constraints.Video(&p) | ||||
| 		track, err := selectScreen(p) | ||||
| 		if err != nil { | ||||
| 			cleanTrackers() | ||||
| 			cleanTracks() | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		trackers = append(trackers, tracker) | ||||
| 		tracks = append(tracks, track) | ||||
| 	} | ||||
|  | ||||
| 	s, err := NewMediaStream(trackers...) | ||||
| 	s, err := NewMediaStream(tracks...) | ||||
| 	if err != nil { | ||||
| 		cleanTrackers() | ||||
| 		cleanTracks() | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -48,41 +47,41 @@ func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) { | ||||
| // with tracks containing the requested types of media. | ||||
| // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia | ||||
| func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) { | ||||
| 	// TODO: It should return media stream based on constraints | ||||
| 	trackers := make([]Track, 0) | ||||
| 	tracks := make([]Track, 0) | ||||
|  | ||||
| 	cleanTrackers := func() { | ||||
| 		for _, t := range trackers { | ||||
| 			t.Close() | ||||
| 	cleanTracks := func() { | ||||
| 		for _, t := range tracks { | ||||
| 			t.Stop() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var videoConstraints, audioConstraints MediaTrackConstraints | ||||
| 	if constraints.Video != nil { | ||||
| 		constraints.Video(&videoConstraints) | ||||
| 		tracker, err := selectVideo(videoConstraints, constraints.Codec) | ||||
| 		var p MediaTrackConstraints | ||||
| 		constraints.Video(&p) | ||||
| 		track, err := selectVideo(p) | ||||
| 		if err != nil { | ||||
| 			cleanTrackers() | ||||
| 			cleanTracks() | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		trackers = append(trackers, tracker) | ||||
| 		tracks = append(tracks, track) | ||||
| 	} | ||||
|  | ||||
| 	if constraints.Audio != nil { | ||||
| 		constraints.Audio(&audioConstraints) | ||||
| 		tracker, err := selectAudio(audioConstraints, constraints.Codec) | ||||
| 		var p MediaTrackConstraints | ||||
| 		constraints.Audio(&p) | ||||
| 		track, err := selectAudio(p) | ||||
| 		if err != nil { | ||||
| 			cleanTrackers() | ||||
| 			cleanTracks() | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		trackers = append(trackers, tracker) | ||||
| 		tracks = append(tracks, track) | ||||
| 	} | ||||
|  | ||||
| 	s, err := NewMediaStream(trackers...) | ||||
| 	s, err := NewMediaStream(tracks...) | ||||
| 	if err != nil { | ||||
| 		cleanTrackers() | ||||
| 		cleanTracks() | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -120,15 +119,12 @@ func queryDriverProperties(filter driver.FilterFn) map[driver.Driver][]prop.Medi | ||||
| func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) { | ||||
| 	var bestDriver driver.Driver | ||||
| 	var bestProp prop.Media | ||||
| 	var foundPropertiesLog []string | ||||
| 	minFitnessDist := math.Inf(1) | ||||
|  | ||||
| 	foundPropertiesLog = append(foundPropertiesLog, "\n============ Found Properties ============") | ||||
| 	driverProperties := queryDriverProperties(filter) | ||||
| 	for d, props := range driverProperties { | ||||
| 		priority := float64(d.Info().Priority) | ||||
| 		for _, p := range props { | ||||
| 			foundPropertiesLog = append(foundPropertiesLog, p.String()) | ||||
| 			fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p) | ||||
| 			if !ok { | ||||
| 				continue | ||||
| @@ -142,25 +138,17 @@ func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	foundPropertiesLog = append(foundPropertiesLog, "=============== Constraints ==============") | ||||
| 	foundPropertiesLog = append(foundPropertiesLog, constraints.String()) | ||||
| 	foundPropertiesLog = append(foundPropertiesLog, "================ Best Fit ================") | ||||
|  | ||||
| 	if bestDriver == nil { | ||||
| 		foundPropertiesLog = append(foundPropertiesLog, "Not found") | ||||
| 		logger.Debug(strings.Join(foundPropertiesLog, "\n\n")) | ||||
| 		return nil, MediaTrackConstraints{}, errNotFound | ||||
| 	} | ||||
|  | ||||
| 	foundPropertiesLog = append(foundPropertiesLog, bestProp.String()) | ||||
| 	logger.Debug(strings.Join(foundPropertiesLog, "\n\n")) | ||||
| 	constraints.selectedMedia = prop.Media{} | ||||
| 	constraints.selectedMedia.MergeConstraints(constraints.MediaConstraints) | ||||
| 	constraints.selectedMedia.Merge(bestProp) | ||||
| 	return bestDriver, constraints, nil | ||||
| } | ||||
|  | ||||
| func selectAudio(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | ||||
| func selectAudio(constraints MediaTrackConstraints) (Track, error) { | ||||
| 	typeFilter := driver.FilterAudioRecorder() | ||||
|  | ||||
| 	d, c, err := selectBestDriver(typeFilter, constraints) | ||||
| @@ -168,9 +156,10 @@ func selectAudio(constraints MediaTrackConstraints, selector *CodecSelector) (Tr | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return newTrackFromDriver(d, c, selector) | ||||
| 	return newAudioTrack(d, c) | ||||
| } | ||||
| func selectVideo(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | ||||
|  | ||||
| func selectVideo(constraints MediaTrackConstraints) (Track, error) { | ||||
| 	typeFilter := driver.FilterVideoRecorder() | ||||
| 	notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen)) | ||||
| 	filter := driver.FilterAnd(typeFilter, notScreenFilter) | ||||
| @@ -180,10 +169,10 @@ func selectVideo(constraints MediaTrackConstraints, selector *CodecSelector) (Tr | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return newTrackFromDriver(d, c, selector) | ||||
| 	return newVideoTrack(d, c) | ||||
| } | ||||
|  | ||||
| func selectScreen(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | ||||
| func selectScreen(constraints MediaTrackConstraints) (Track, error) { | ||||
| 	typeFilter := driver.FilterVideoRecorder() | ||||
| 	screenFilter := driver.FilterDeviceType(driver.Screen) | ||||
| 	filter := driver.FilterAnd(typeFilter, screenFilter) | ||||
| @@ -193,7 +182,7 @@ func selectScreen(constraints MediaTrackConstraints, selector *CodecSelector) (T | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return newTrackFromDriver(d, c, selector) | ||||
| 	return newVideoTrack(d, c) | ||||
| } | ||||
|  | ||||
| func EnumerateDevices() []MediaDeviceInfo { | ||||
|   | ||||
| @@ -1,82 +0,0 @@ | ||||
| // +build e2e | ||||
|  | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"image" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec/x264" | ||||
| 	"github.com/pion/mediadevices/pkg/frame" | ||||
| ) | ||||
|  | ||||
| type mockVideoSource struct { | ||||
| 	width, height int | ||||
| 	pool          sync.Pool | ||||
| 	decoder       frame.Decoder | ||||
| } | ||||
|  | ||||
| func newMockVideoSource(width, height int) *mockVideoSource { | ||||
| 	decoder, err := frame.NewDecoder(frame.FormatYUY2) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	return &mockVideoSource{ | ||||
| 		width:  width, | ||||
| 		height: height, | ||||
| 		pool: sync.Pool{ | ||||
| 			New: func() interface{} { | ||||
| 				resolution := width * height | ||||
| 				return make([]byte, resolution*2) | ||||
| 			}, | ||||
| 		}, | ||||
| 		decoder: decoder, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (source *mockVideoSource) ID() string   { return "" } | ||||
| func (source *mockVideoSource) Close() error { return nil } | ||||
| func (source *mockVideoSource) Read() (image.Image, func(), error) { | ||||
| 	raw := source.pool.Get().([]byte) | ||||
| 	decoded, release, err := source.decoder.Decode(raw, source.width, source.height) | ||||
| 	source.pool.Put(raw) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	return decoded, release, nil | ||||
| } | ||||
|  | ||||
| func BenchmarkEndToEnd(b *testing.B) { | ||||
| 	params, err := x264.NewParams() | ||||
| 	if err != nil { | ||||
| 		b.Fatal(err) | ||||
| 	} | ||||
| 	params.BitRate = 300_000 | ||||
|  | ||||
| 	videoSource := newMockVideoSource(1920, 1080) | ||||
| 	track := NewVideoTrack(videoSource, nil).(*VideoTrack) | ||||
| 	defer track.Close() | ||||
|  | ||||
| 	reader := track.NewReader(false) | ||||
| 	inputProp, err := detectCurrentVideoProp(track.Broadcaster) | ||||
| 	if err != nil { | ||||
| 		b.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	encodedReader, err := params.BuildVideoEncoder(reader, inputProp) | ||||
| 	if err != nil { | ||||
| 		b.Fatal(err) | ||||
| 	} | ||||
| 	defer encodedReader.Close() | ||||
|  | ||||
| 	for i := 0; i < b.N; i++ { | ||||
| 		_, release, err := encodedReader.Read() | ||||
| 		if err != nil { | ||||
| 			b.Fatal(err) | ||||
| 		} | ||||
| 		release() | ||||
| 	} | ||||
| } | ||||
| @@ -1,42 +1,123 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| 	"github.com/pion/webrtc/v2/pkg/media" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/driver" | ||||
| 	_ "github.com/pion/mediadevices/pkg/driver/audiotest" | ||||
| 	_ "github.com/pion/mediadevices/pkg/driver/videotest" | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
|  | ||||
| func TestGetUserMedia(t *testing.T) { | ||||
| 	brokenVideoParams := mockParams{ | ||||
| 		name: "MockVideo", | ||||
| 	} | ||||
| 	videoParams := brokenVideoParams | ||||
| 	videoParams.BitRate = 100000 | ||||
| 	audioParams := mockParams{ | ||||
| 		BaseParams: codec.BaseParams{ | ||||
| 			BitRate: 32000, | ||||
| 		}, | ||||
| 		name: "MockAudio", | ||||
| 	} | ||||
| 	constraints := MediaStreamConstraints{ | ||||
| 		Video: func(p *prop.Media) { | ||||
| 			p.Width = 640 | ||||
| 			p.Height = 480 | ||||
| 		}, | ||||
| 		Audio: func(p *prop.Media) {}, | ||||
| 	} | ||||
|  | ||||
| 	md := NewMediaDevicesFromCodecs( | ||||
| 		map[webrtc.RTPCodecType][]*webrtc.RTPCodec{ | ||||
| 			webrtc.RTPCodecTypeVideo: { | ||||
| 				{Type: webrtc.RTPCodecTypeVideo, Name: "MockVideo", PayloadType: 1}, | ||||
| 			}, | ||||
| 			webrtc.RTPCodecTypeAudio: { | ||||
| 				{Type: webrtc.RTPCodecTypeAudio, Name: "MockAudio", PayloadType: 2}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		WithTrackGenerator( | ||||
| 			func(_ uint8, _ uint32, id, _ string, codec *webrtc.RTPCodec) ( | ||||
| 				LocalTrack, error, | ||||
| 			) { | ||||
| 				return newMockTrack(codec, id), nil | ||||
| 			}, | ||||
| 		), | ||||
| 		WithVideoEncoders(&brokenVideoParams), | ||||
| 		WithAudioEncoders(&audioParams), | ||||
| 	) | ||||
| <<<<<<< HEAD | ||||
| 	constraints := MediaStreamConstraints{ | ||||
| 		Video: func(c *MediaTrackConstraints) { | ||||
| 			c.Enabled = true | ||||
| 			c.Width = prop.Int(640) | ||||
| 			c.Height = prop.Int(480) | ||||
| 			params := videoParams | ||||
| 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{¶ms} | ||||
| 		}, | ||||
| 		Audio: func(c *MediaTrackConstraints) { | ||||
| 			c.Enabled = true | ||||
| 			params := audioParams | ||||
| 			c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms} | ||||
| 		}, | ||||
| 	} | ||||
| 	constraintsWrong := MediaStreamConstraints{ | ||||
| 		Video: func(c *MediaTrackConstraints) { | ||||
| 			c.Width = prop.IntExact(10000) | ||||
| 			c.Enabled = true | ||||
| 			c.Width = prop.Int(640) | ||||
| 			c.Height = prop.Int(480) | ||||
| 			params := videoParams | ||||
| 			params.BitRate = 0 | ||||
| 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{¶ms} | ||||
| 		}, | ||||
| 		Audio: func(c *MediaTrackConstraints) { | ||||
| 			c.Enabled = true | ||||
| 			params := audioParams | ||||
| 			c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms} | ||||
| 		}, | ||||
| 	} | ||||
| ======= | ||||
| >>>>>>> ccd7985... Redesign GetUserMedia API | ||||
|  | ||||
| 	// GetUserMedia with broken parameters | ||||
| 	ms, err := GetUserMedia(constraintsWrong) | ||||
| 	ms, err := md.GetUserMedia(constraints) | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Expected error, but got nil") | ||||
| 	} | ||||
|  | ||||
| 	md = NewMediaDevicesFromCodecs( | ||||
| 		map[webrtc.RTPCodecType][]*webrtc.RTPCodec{ | ||||
| 			webrtc.RTPCodecTypeVideo: []*webrtc.RTPCodec{ | ||||
| 				&webrtc.RTPCodec{Type: webrtc.RTPCodecTypeVideo, Name: "MockVideo", PayloadType: 1}, | ||||
| 			}, | ||||
| 			webrtc.RTPCodecTypeAudio: []*webrtc.RTPCodec{ | ||||
| 				&webrtc.RTPCodec{Type: webrtc.RTPCodecTypeAudio, Name: "MockAudio", PayloadType: 2}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		WithTrackGenerator( | ||||
| 			func(_ uint8, _ uint32, id, _ string, codec *webrtc.RTPCodec) ( | ||||
| 				LocalTrack, error, | ||||
| 			) { | ||||
| 				return newMockTrack(codec, id), nil | ||||
| 			}, | ||||
| 		), | ||||
| 		WithVideoEncoders(&videoParams), | ||||
| 		WithAudioEncoders(&audioParams), | ||||
| 	) | ||||
|  | ||||
| 	// GetUserMedia with correct parameters | ||||
| 	ms, err = GetUserMedia(constraints) | ||||
| 	ms, err = md.GetUserMedia(constraints) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Unexpected error: %v", err) | ||||
| 	} | ||||
| @@ -54,11 +135,11 @@ func TestGetUserMedia(t *testing.T) { | ||||
| 	time.Sleep(50 * time.Millisecond) | ||||
|  | ||||
| 	for _, track := range tracks { | ||||
| 		track.Close() | ||||
| 		track.Stop() | ||||
| 	} | ||||
|  | ||||
| 	// Stop and retry GetUserMedia | ||||
| 	ms, err = GetUserMedia(constraints) | ||||
| 	ms, err = md.GetUserMedia(constraints) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to GetUserMedia after the previsous tracks stopped: %v", err) | ||||
| 	} | ||||
| @@ -75,10 +156,104 @@ func TestGetUserMedia(t *testing.T) { | ||||
| 	} | ||||
| 	time.Sleep(50 * time.Millisecond) | ||||
| 	for _, track := range tracks { | ||||
| 		track.Close() | ||||
| 		track.Stop() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type mockTrack struct { | ||||
| 	codec *webrtc.RTPCodec | ||||
| 	id    string | ||||
| } | ||||
|  | ||||
| func newMockTrack(codec *webrtc.RTPCodec, id string) *mockTrack { | ||||
| 	return &mockTrack{ | ||||
| 		codec: codec, | ||||
| 		id:    id, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *mockTrack) WriteSample(s media.Sample) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (t *mockTrack) Codec() *webrtc.RTPCodec { | ||||
| 	return t.codec | ||||
| } | ||||
|  | ||||
| func (t *mockTrack) ID() string { | ||||
| 	return t.id | ||||
| } | ||||
|  | ||||
| func (t *mockTrack) Kind() webrtc.RTPCodecType { | ||||
| 	return t.codec.Type | ||||
| } | ||||
|  | ||||
| type mockParams struct { | ||||
| 	codec.BaseParams | ||||
| 	name string | ||||
| } | ||||
|  | ||||
| func (params *mockParams) Name() string { | ||||
| 	return params.name | ||||
| } | ||||
|  | ||||
| func (params *mockParams) BuildVideoEncoder(r video.Reader, p prop.Media) (codec.ReadCloser, error) { | ||||
| 	if params.BitRate == 0 { | ||||
| 		// This is a dummy error to test the failure condition. | ||||
| 		return nil, errors.New("wrong codec parameter") | ||||
| 	} | ||||
| 	return &mockVideoCodec{ | ||||
| 		r:      r, | ||||
| 		closed: make(chan struct{}), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (params *mockParams) BuildAudioEncoder(r audio.Reader, p prop.Media) (codec.ReadCloser, error) { | ||||
| 	return &mockAudioCodec{ | ||||
| 		r:      r, | ||||
| 		closed: make(chan struct{}), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| type mockCodec struct{} | ||||
|  | ||||
| func (e *mockCodec) SetBitRate(b int) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (e *mockCodec) ForceKeyFrame() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type mockVideoCodec struct { | ||||
| 	mockCodec | ||||
| 	r      video.Reader | ||||
| 	closed chan struct{} | ||||
| } | ||||
|  | ||||
| func (m *mockVideoCodec) Read(b []byte) (int, error) { | ||||
| 	if _, err := m.r.Read(); err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return len(b), nil | ||||
| } | ||||
|  | ||||
| func (m *mockVideoCodec) Close() error { return nil } | ||||
|  | ||||
| type mockAudioCodec struct { | ||||
| 	mockCodec | ||||
| 	r      audio.Reader | ||||
| 	closed chan struct{} | ||||
| } | ||||
|  | ||||
| func (m *mockAudioCodec) Read(b []byte) (int, error) { | ||||
| 	if _, err := m.r.Read(); err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	return len(b), nil | ||||
| } | ||||
| func (m *mockAudioCodec) Close() error { return nil } | ||||
|  | ||||
| func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) { | ||||
| 	filterFn := driver.FilterVideoRecorder() | ||||
| 	drivers := driver.GetManager().Query(filterFn) | ||||
|   | ||||
| @@ -2,6 +2,8 @@ package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // MediaStream is an interface that represents a collection of existing tracks. | ||||
| @@ -19,20 +21,21 @@ type MediaStream interface { | ||||
| } | ||||
|  | ||||
| type mediaStream struct { | ||||
| 	tracks map[Track]struct{} | ||||
| 	tracks map[string]Track | ||||
| 	l      sync.RWMutex | ||||
| } | ||||
|  | ||||
| const trackTypeDefault MediaDeviceType = 0 | ||||
| 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 ...Track) (MediaStream, error) { | ||||
| 	m := mediaStream{tracks: make(map[Track]struct{})} | ||||
| 	m := mediaStream{tracks: make(map[string]Track)} | ||||
|  | ||||
| 	for _, track := range tracks { | ||||
| 		if _, ok := m.tracks[track]; !ok { | ||||
| 			m.tracks[track] = struct{}{} | ||||
| 		id := track.ID() | ||||
| 		if _, ok := m.tracks[id]; !ok { | ||||
| 			m.tracks[id] = track | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -40,26 +43,26 @@ func NewMediaStream(tracks ...Track) (MediaStream, error) { | ||||
| } | ||||
|  | ||||
| func (m *mediaStream) GetAudioTracks() []Track { | ||||
| 	return m.queryTracks(AudioInput) | ||||
| 	return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindAudio }) | ||||
| } | ||||
|  | ||||
| func (m *mediaStream) GetVideoTracks() []Track { | ||||
| 	return m.queryTracks(VideoInput) | ||||
| 	return m.queryTracks(func(t Track) bool { return t.Kind() == TrackKindVideo }) | ||||
| } | ||||
|  | ||||
| func (m *mediaStream) GetTracks() []Track { | ||||
| 	return m.queryTracks(trackTypeDefault) | ||||
| 	return m.queryTracks(func(t Track) bool { return true }) | ||||
| } | ||||
|  | ||||
| // 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 MediaDeviceType) []Track { | ||||
| func (m *mediaStream) queryTracks(filter func(track Track) bool) []Track { | ||||
| 	m.l.RLock() | ||||
| 	defer m.l.RUnlock() | ||||
|  | ||||
| 	result := make([]Track, 0) | ||||
| 	for track := range m.tracks { | ||||
| 		if track.Kind() == t || t == trackTypeDefault { | ||||
| 	for _, track := range m.tracks { | ||||
| 		if filter(track) { | ||||
| 			result = append(result, track) | ||||
| 		} | ||||
| 	} | ||||
| @@ -71,16 +74,17 @@ func (m *mediaStream) AddTrack(t Track) { | ||||
| 	m.l.Lock() | ||||
| 	defer m.l.Unlock() | ||||
|  | ||||
| 	if _, ok := m.tracks[t]; ok { | ||||
| 	id := t.ID() | ||||
| 	if _, ok := m.tracks[id]; ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	m.tracks[t] = struct{}{} | ||||
| 	m.tracks[id] = t | ||||
| } | ||||
|  | ||||
| func (m *mediaStream) RemoveTrack(t Track) { | ||||
| 	m.l.Lock() | ||||
| 	defer m.l.Unlock() | ||||
|  | ||||
| 	delete(m.tracks, t) | ||||
| 	delete(m.tracks, t.ID()) | ||||
| } | ||||
|   | ||||
| @@ -1,88 +0,0 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| type mockMediaStreamTrack struct { | ||||
| 	kind MediaDeviceType | ||||
| } | ||||
|  | ||||
| func (track *mockMediaStreamTrack) ID() string { | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (track *mockMediaStreamTrack) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (track *mockMediaStreamTrack) Kind() MediaDeviceType { | ||||
| 	return track.kind | ||||
| } | ||||
|  | ||||
| func (track *mockMediaStreamTrack) OnEnded(handler func(error)) { | ||||
| } | ||||
|  | ||||
| func (track *mockMediaStreamTrack) Bind(pc *webrtc.PeerConnection) (*webrtc.Track, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (track *mockMediaStreamTrack) Unbind(pc *webrtc.PeerConnection) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestMediaStreamFilters(t *testing.T) { | ||||
| 	audioTracks := []Track{ | ||||
| 		&mockMediaStreamTrack{AudioInput}, | ||||
| 		&mockMediaStreamTrack{AudioInput}, | ||||
| 		&mockMediaStreamTrack{AudioInput}, | ||||
| 		&mockMediaStreamTrack{AudioInput}, | ||||
| 		&mockMediaStreamTrack{AudioInput}, | ||||
| 	} | ||||
|  | ||||
| 	videoTracks := []Track{ | ||||
| 		&mockMediaStreamTrack{VideoInput}, | ||||
| 		&mockMediaStreamTrack{VideoInput}, | ||||
| 		&mockMediaStreamTrack{VideoInput}, | ||||
| 	} | ||||
|  | ||||
| 	tracks := append(audioTracks, videoTracks...) | ||||
| 	stream, err := NewMediaStream(tracks...) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	expect := func(t *testing.T, actual, expected []Track) { | ||||
| 		if len(actual) != len(expected) { | ||||
| 			t.Fatalf("%s: Expected to get %d trackers, but got %d trackers", t.Name(), len(expected), len(actual)) | ||||
| 		} | ||||
|  | ||||
| 		for _, a := range actual { | ||||
| 			found := false | ||||
| 			for _, e := range expected { | ||||
| 				if e == a { | ||||
| 					found = true | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if !found { | ||||
| 				t.Fatalf("%s: Expected to find %p in the query results", t.Name(), a) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	t.Run("GetAudioTracks", func(t *testing.T) { | ||||
| 		expect(t, stream.GetAudioTracks(), audioTracks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("GetVideoTracks", func(t *testing.T) { | ||||
| 		expect(t, stream.GetVideoTracks(), videoTracks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("GetTracks", func(t *testing.T) { | ||||
| 		expect(t, stream.GetTracks(), tracks) | ||||
| 	}) | ||||
| } | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| type MediaStreamConstraints struct { | ||||
| 	Audio MediaOption | ||||
| 	Video MediaOption | ||||
| 	Codec *CodecSelector | ||||
| } | ||||
|  | ||||
| // MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints | ||||
|   | ||||
							
								
								
									
										35
									
								
								meta.go
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								meta.go
									
									
									
									
									
								
							| @@ -1,35 +0,0 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
|  | ||||
| // detectCurrentVideoProp is a small helper to get current video property | ||||
| func detectCurrentVideoProp(broadcaster *video.Broadcaster) (prop.Media, error) { | ||||
| 	var currentProp prop.Media | ||||
|  | ||||
| 	// Since broadcaster has a ring buffer internally, a new reader will either read the last | ||||
| 	// buffered frame or a new frame from the source. This also implies that no frame will be lost | ||||
| 	// in any case. | ||||
| 	metaReader := broadcaster.NewReader(false) | ||||
| 	metaReader = video.DetectChanges(0, func(p prop.Media) { currentProp = p })(metaReader) | ||||
| 	_, _, err := metaReader.Read() | ||||
|  | ||||
| 	return currentProp, err | ||||
| } | ||||
|  | ||||
| // detectCurrentAudioProp is a small helper to get current audio property | ||||
| func detectCurrentAudioProp(broadcaster *audio.Broadcaster) (prop.Media, error) { | ||||
| 	var currentProp prop.Media | ||||
|  | ||||
| 	// Since broadcaster has a ring buffer internally, a new reader will either read the last | ||||
| 	// buffered frame or a new frame from the source. This also implies that no frame will be lost | ||||
| 	// in any case. | ||||
| 	metaReader := broadcaster.NewReader(false) | ||||
| 	metaReader = audio.DetectChanges(0, func(p prop.Media) { currentProp = p })(metaReader) | ||||
| 	_, _, err := metaReader.Read() | ||||
|  | ||||
| 	return currentProp, err | ||||
| } | ||||
							
								
								
									
										98
									
								
								meta_test.go
									
									
									
									
									
								
							
							
						
						
									
										98
									
								
								meta_test.go
									
									
									
									
									
								
							| @@ -1,98 +0,0 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"image" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| func TestDetectCurrentVideoProp(t *testing.T) { | ||||
| 	resolution := image.Rect(0, 0, 4, 4) | ||||
| 	first := image.NewRGBA(resolution) | ||||
| 	first.Pix[0] = 1 | ||||
| 	second := image.NewRGBA(resolution) | ||||
| 	second.Pix[0] = 2 | ||||
|  | ||||
| 	isFirst := true | ||||
| 	source := video.ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		if isFirst { | ||||
| 			isFirst = true | ||||
| 			return first, func() {}, nil | ||||
| 		} else { | ||||
| 			return second, func() {}, nil | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	broadcaster := video.NewBroadcaster(source, nil) | ||||
|  | ||||
| 	currentProp, err := detectCurrentVideoProp(broadcaster) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if currentProp.Width != resolution.Dx() { | ||||
| 		t.Fatalf("Expect the actual width to be %d, but got %d", currentProp.Width, resolution.Dx()) | ||||
| 	} | ||||
|  | ||||
| 	if currentProp.Height != resolution.Dy() { | ||||
| 		t.Fatalf("Expect the actual height to be %d, but got %d", currentProp.Height, resolution.Dy()) | ||||
| 	} | ||||
|  | ||||
| 	reader := broadcaster.NewReader(false) | ||||
| 	img, _, err := reader.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	rgba := img.(*image.RGBA) | ||||
| 	if rgba.Pix[0] != 1 { | ||||
| 		t.Fatal("Expect the frame after reading the current prop is not the first frame") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDetectCurrentAudioProp(t *testing.T) { | ||||
| 	info := wave.ChunkInfo{ | ||||
| 		Len:          4, | ||||
| 		Channels:     2, | ||||
| 		SamplingRate: 48000, | ||||
| 	} | ||||
| 	first := wave.NewInt16Interleaved(info) | ||||
| 	first.Data[0] = 1 | ||||
| 	second := wave.NewInt16Interleaved(info) | ||||
| 	second.Data[0] = 2 | ||||
|  | ||||
| 	isFirst := true | ||||
| 	source := audio.ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 		if isFirst { | ||||
| 			isFirst = true | ||||
| 			return first, func() {}, nil | ||||
| 		} else { | ||||
| 			return second, func() {}, nil | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	broadcaster := audio.NewBroadcaster(source, nil) | ||||
|  | ||||
| 	currentProp, err := detectCurrentAudioProp(broadcaster) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if currentProp.ChannelCount != info.Channels { | ||||
| 		t.Fatalf("Expect the actual channel count to be %d, but got %d", currentProp.ChannelCount, info.Channels) | ||||
| 	} | ||||
|  | ||||
| 	reader := broadcaster.NewReader(false) | ||||
| 	chunk, _, err := reader.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	realChunk := chunk.(*wave.Int16Interleaved) | ||||
| 	if realChunk.Data[0] != 1 { | ||||
| 		t.Fatal("Expect the chunk after reading the current prop is not the first chunk") | ||||
| 	} | ||||
| } | ||||
| @@ -110,12 +110,12 @@ func (rc *ReadCloser) dataCb(data []byte) { | ||||
| // Read reads raw data, the format is determined by the media type and property: | ||||
| //   - For video, each call will return a frame. | ||||
| //   - For audio, each call will return a chunk which its size configured by Latency | ||||
| func (rc *ReadCloser) Read() ([]byte, func(), error) { | ||||
| func (rc *ReadCloser) Read() ([]byte, error) { | ||||
| 	data, ok := <-rc.dataChan | ||||
| 	if !ok { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return nil, io.EOF | ||||
| 	} | ||||
| 	return data, func() {}, nil | ||||
| 	return data, nil | ||||
| } | ||||
|  | ||||
| // Close closes the capturing session, and no data will flow anymore | ||||
|   | ||||
| @@ -1,45 +1,21 @@ | ||||
| package codec | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // RTPCodec wraps webrtc.RTPCodec. RTPCodec might extend webrtc.RTPCodec in the future. | ||||
| type RTPCodec struct { | ||||
| 	*webrtc.RTPCodec | ||||
| } | ||||
|  | ||||
| // NewRTPH264Codec is a helper to create an H264 codec | ||||
| func NewRTPH264Codec(clockrate uint32) *RTPCodec { | ||||
| 	return &RTPCodec{webrtc.NewRTPH264Codec(webrtc.DefaultPayloadTypeH264, clockrate)} | ||||
| } | ||||
|  | ||||
| // NewRTPVP8Codec is a helper to create an VP8 codec | ||||
| func NewRTPVP8Codec(clockrate uint32) *RTPCodec { | ||||
| 	return &RTPCodec{webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, clockrate)} | ||||
| } | ||||
|  | ||||
| // NewRTPVP9Codec is a helper to create an VP9 codec | ||||
| func NewRTPVP9Codec(clockrate uint32) *RTPCodec { | ||||
| 	return &RTPCodec{webrtc.NewRTPVP9Codec(webrtc.DefaultPayloadTypeVP9, clockrate)} | ||||
| } | ||||
|  | ||||
| // NewRTPOpusCodec is a helper to create an Opus codec | ||||
| func NewRTPOpusCodec(clockrate uint32) *RTPCodec { | ||||
| 	return &RTPCodec{webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, clockrate)} | ||||
| } | ||||
|  | ||||
| // AudioEncoderBuilder is the interface that wraps basic operations that are | ||||
| // necessary to build the audio encoder. | ||||
| // | ||||
| // This interface is for codec implementors to provide codec specific params, | ||||
| // but still giving generality for the users. | ||||
| type AudioEncoderBuilder interface { | ||||
| 	// RTPCodec represents the codec metadata | ||||
| 	RTPCodec() *RTPCodec | ||||
| 	// Name represents the codec name | ||||
| 	Name() string | ||||
| 	// BuildAudioEncoder builds audio encoder by given media params and audio input | ||||
| 	BuildAudioEncoder(r audio.Reader, p prop.Media) (ReadCloser, error) | ||||
| } | ||||
| @@ -50,16 +26,15 @@ type AudioEncoderBuilder interface { | ||||
| // This interface is for codec implementors to provide codec specific params, | ||||
| // but still giving generality for the users. | ||||
| type VideoEncoderBuilder interface { | ||||
| 	// RTPCodec represents the codec metadata | ||||
| 	RTPCodec() *RTPCodec | ||||
| 	// Name represents the codec name | ||||
| 	Name() string | ||||
| 	// BuildVideoEncoder builds video encoder by given media params and video input | ||||
| 	BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error) | ||||
| } | ||||
|  | ||||
| // ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame | ||||
| type ReadCloser interface { | ||||
| 	Read() (b []byte, release func(), err error) | ||||
| 	Close() error | ||||
| 	io.ReadCloser | ||||
| 	// SetBitRate sets current target bitrate, lower bitrate means smaller data will be transmitted | ||||
| 	// but this also means that the quality will also be lower. | ||||
| 	SetBitRate(int) error | ||||
|   | ||||
| @@ -1,196 +0,0 @@ | ||||
| #include <interface/mmal/mmal.h> | ||||
| #include <interface/mmal/util/mmal_default_components.h> | ||||
| #include <interface/mmal/util/mmal_util_params.h> | ||||
| #include <stdbool.h> | ||||
| #include <stdint.h> | ||||
| #include <stdlib.h> | ||||
|  | ||||
| #define CHK(__status, __msg)                                                                                           \ | ||||
|   do {                                                                                                                 \ | ||||
|     status.code = __status;                                                                                            \ | ||||
|     if (status.code != MMAL_SUCCESS) {                                                                                 \ | ||||
|       status.msg = __msg;                                                                                              \ | ||||
|       goto CleanUp;                                                                                                    \ | ||||
|     }                                                                                                                  \ | ||||
|   } while (0) | ||||
|  | ||||
| typedef struct Status { | ||||
|   MMAL_STATUS_T code; | ||||
|   const char *msg; | ||||
| } Status; | ||||
|  | ||||
| typedef struct Slice { | ||||
|   uint8_t *data; | ||||
|   int len; | ||||
| } Slice; | ||||
|  | ||||
| typedef struct Params { | ||||
|   int width, height; | ||||
|   uint32_t bitrate; | ||||
|   uint32_t key_frame_interval; | ||||
| } Params; | ||||
|  | ||||
| typedef struct Encoder { | ||||
|   MMAL_COMPONENT_T *component; | ||||
|   MMAL_PORT_T *port_in, *port_out; | ||||
|   MMAL_QUEUE_T *queue_out; | ||||
|   MMAL_POOL_T *pool_in, *pool_out; | ||||
| } Encoder; | ||||
|  | ||||
| Status enc_new(Params, Encoder *); | ||||
| Status enc_encode(Encoder *, Slice y, Slice cb, Slice cr, MMAL_BUFFER_HEADER_T **); | ||||
| Status enc_close(Encoder *); | ||||
|  | ||||
| static void encoder_in_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer) { mmal_buffer_header_release(buffer); } | ||||
|  | ||||
| static void encoder_out_cb(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer) { | ||||
|   MMAL_QUEUE_T *queue = (MMAL_QUEUE_T *)port->userdata; | ||||
|   mmal_queue_put(queue, buffer); | ||||
| } | ||||
|  | ||||
| Status enc_new(Params params, Encoder *encoder) { | ||||
|   Status status = {0}; | ||||
|   bool created = false; | ||||
|  | ||||
|   memset(encoder, 0, sizeof(Encoder)); | ||||
|  | ||||
|   CHK(mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_ENCODER, &encoder->component), | ||||
|       "Failed to create video encoder component"); | ||||
|   created = true; | ||||
|  | ||||
|   encoder->port_in = encoder->component->input[0]; | ||||
|   encoder->port_in->format->type = MMAL_ES_TYPE_VIDEO; | ||||
|   encoder->port_in->format->encoding = MMAL_ENCODING_I420; | ||||
|   encoder->port_in->format->es->video.width = params.width; | ||||
|   encoder->port_in->format->es->video.height = params.height; | ||||
|   encoder->port_in->format->es->video.par.num = 1; | ||||
|   encoder->port_in->format->es->video.par.den = 1; | ||||
|   encoder->port_in->format->es->video.crop.x = 0; | ||||
|   encoder->port_in->format->es->video.crop.y = 0; | ||||
|   encoder->port_in->format->es->video.crop.width = params.width; | ||||
|   encoder->port_in->format->es->video.crop.height = params.height; | ||||
|   CHK(mmal_port_format_commit(encoder->port_in), "Failed to commit input port format"); | ||||
|  | ||||
|   encoder->port_out = encoder->component->output[0]; | ||||
|   encoder->port_out->format->type = MMAL_ES_TYPE_VIDEO; | ||||
|   encoder->port_out->format->encoding = MMAL_ENCODING_H264; | ||||
|   encoder->port_out->format->bitrate = params.bitrate; | ||||
|   CHK(mmal_port_format_commit(encoder->port_out), "Failed to commit output port format"); | ||||
|  | ||||
|   MMAL_PARAMETER_VIDEO_PROFILE_T encoder_param_profile = {0}; | ||||
|   encoder_param_profile.hdr.id = MMAL_PARAMETER_PROFILE; | ||||
|   encoder_param_profile.hdr.size = sizeof(encoder_param_profile); | ||||
|   encoder_param_profile.profile[0].profile = MMAL_VIDEO_PROFILE_H264_BASELINE; | ||||
|   encoder_param_profile.profile[0].level = MMAL_VIDEO_LEVEL_H264_42; | ||||
|   CHK(mmal_port_parameter_set(encoder->port_out, &encoder_param_profile.hdr), "Failed to set encoder profile param"); | ||||
|  | ||||
|   CHK(mmal_port_parameter_set_uint32(encoder->port_out, MMAL_PARAMETER_INTRAPERIOD, params.key_frame_interval), | ||||
|       "Failed to set intra period param"); | ||||
|  | ||||
|   MMAL_PARAMETER_VIDEO_RATECONTROL_T encoder_param_rate_control = {0}; | ||||
|   encoder_param_rate_control.hdr.id = MMAL_PARAMETER_RATECONTROL; | ||||
|   encoder_param_rate_control.hdr.size = sizeof(encoder_param_rate_control); | ||||
|   encoder_param_rate_control.control = MMAL_VIDEO_RATECONTROL_VARIABLE; | ||||
|   CHK(mmal_port_parameter_set(encoder->port_out, &encoder_param_rate_control.hdr), "Failed to set rate control param"); | ||||
|  | ||||
|   // Some decoders expect SPS/PPS headers to be added to every frame | ||||
|   CHK(mmal_port_parameter_set_boolean(encoder->port_out, MMAL_PARAMETER_VIDEO_ENCODE_INLINE_HEADER, MMAL_TRUE), | ||||
|       "Failed to set inline header param"); | ||||
|  | ||||
|   CHK(mmal_port_parameter_set_boolean(encoder->port_out, MMAL_PARAMETER_VIDEO_ENCODE_HEADERS_WITH_FRAME, MMAL_TRUE), | ||||
|       "Failed to set headers with frame param"); | ||||
|  | ||||
|   /* FIXME: Somehow this flag is broken? When this flag is on, the encoder will get stuck. | ||||
|   // Since our use case is mainly for real time streaming, the encoder should optimized for low latency | ||||
|   CHK(mmal_port_parameter_set_boolean(encoder->port_out, MMAL_PARAMETER_VIDEO_ENCODE_H264_LOW_LATENCY, MMAL_TRUE), | ||||
|       "Failed to set low latency param"); | ||||
|   */ | ||||
|  | ||||
|   // Now we know the format of both ports and the requirements of the encoder, we can create | ||||
|   // our buffer headers and their associated memory buffers. We use the buffer pool API for this. | ||||
|   encoder->port_in->buffer_num = encoder->port_in->buffer_num_min; | ||||
|   // mmal calculates recommended size that's big enough to store all of the pixels | ||||
|   encoder->port_in->buffer_size = encoder->port_in->buffer_size_recommended; | ||||
|   encoder->pool_in = mmal_pool_create(encoder->port_in->buffer_num, encoder->port_in->buffer_size); | ||||
|   encoder->port_out->buffer_num = encoder->port_out->buffer_num_min; | ||||
|   encoder->port_out->buffer_size = encoder->port_out->buffer_size_recommended; | ||||
|   encoder->pool_out = mmal_pool_create(encoder->port_out->buffer_num, encoder->port_out->buffer_size); | ||||
|  | ||||
|   // Create a queue to store our encoded video frames. The callback we will get when | ||||
|   // a frame has been encoded will put the frame into this queue. | ||||
|   encoder->queue_out = mmal_queue_create(); | ||||
|   encoder->port_out->userdata = (void *)encoder->queue_out; | ||||
|  | ||||
|   // Enable all the input port and the output port. | ||||
|   // The callback specified here is the function which will be called when the buffer header | ||||
|   // we sent to the component has been processed. | ||||
|   CHK(mmal_port_enable(encoder->port_in, encoder_in_cb), "Failed to enable input port"); | ||||
|   CHK(mmal_port_enable(encoder->port_out, encoder_out_cb), "Failed to enable output port"); | ||||
|  | ||||
|   // Enable the component. Components will only process data when they are enabled. | ||||
|   CHK(mmal_component_enable(encoder->component), "Failed to enable component"); | ||||
|  | ||||
| CleanUp: | ||||
|  | ||||
|   if (status.code != MMAL_SUCCESS) { | ||||
|     if (created) { | ||||
|       enc_close(encoder); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| // enc_encode encodes y, cb, cr. The encoded frame is going to be stored in encoded_buffer. | ||||
| // IMPORTANT: the caller is responsible to release the ownership of encoded_buffer | ||||
| Status enc_encode(Encoder *encoder, Slice y, Slice cb, Slice cr, MMAL_BUFFER_HEADER_T **encoded_buffer) { | ||||
|   Status status = {0}; | ||||
|   MMAL_BUFFER_HEADER_T *buffer; | ||||
|   uint32_t required_size; | ||||
|  | ||||
|   // buffer should always be available since the encoding process is blocking | ||||
|   buffer = mmal_queue_get(encoder->pool_in->queue); | ||||
|   assert(buffer != NULL); | ||||
|   // buffer->data should've been allocated with enough memory to contain a frame by pool_in | ||||
|   required_size = y.len + cb.len + cr.len; | ||||
|   assert(buffer->alloc_size >= required_size); | ||||
|   memcpy(buffer->data, y.data, y.len); | ||||
|   memcpy(buffer->data + y.len, cb.data, cb.len); | ||||
|   memcpy(buffer->data + y.len + cb.len, cr.data, cr.len); | ||||
|   buffer->length = required_size; | ||||
|   CHK(mmal_port_send_buffer(encoder->port_in, buffer), "Failed to send filled buffer to input port"); | ||||
|  | ||||
|   while (1) { | ||||
|     // Send empty buffers to the output port to allow the encoder to start | ||||
|     // producing frames as soon as it gets input data | ||||
|     while ((buffer = mmal_queue_get(encoder->pool_out->queue)) != NULL) { | ||||
|       CHK(mmal_port_send_buffer(encoder->port_out, buffer), "Failed to send empty buffers to output port"); | ||||
|     } | ||||
|  | ||||
|     while ((buffer = mmal_queue_wait(encoder->queue_out)) != NULL) { | ||||
|       if ((buffer->flags & MMAL_BUFFER_HEADER_FLAG_FRAME_END) != 0) { | ||||
|         *encoded_buffer = buffer; | ||||
|         goto CleanUp; | ||||
|       } | ||||
|  | ||||
|       mmal_buffer_header_release(buffer); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| CleanUp: | ||||
|  | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| Status enc_close(Encoder *encoder) { | ||||
|   Status status = {0}; | ||||
|  | ||||
|   mmal_pool_destroy(encoder->pool_out); | ||||
|   mmal_pool_destroy(encoder->pool_in); | ||||
|   mmal_queue_destroy(encoder->queue_out); | ||||
|   mmal_component_destroy(encoder->component); | ||||
|  | ||||
| CleanUp: | ||||
|  | ||||
|   return status; | ||||
| } | ||||
| @@ -1,112 +0,0 @@ | ||||
| // Package mmal implements a hardware accelerated H264 encoder for raspberry pi. | ||||
| // This package requires libmmal headers and libraries to be built. | ||||
| // Reference: https://github.com/raspberrypi/userland/tree/master/interface/mmal | ||||
| package mmal | ||||
|  | ||||
| // #cgo pkg-config: mmal | ||||
| // #include "bridge.h" | ||||
| import "C" | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"io" | ||||
| 	"sync" | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
|  | ||||
| type encoder struct { | ||||
| 	engine C.Encoder | ||||
| 	r      video.Reader | ||||
| 	mu     sync.Mutex | ||||
| 	closed bool | ||||
| 	cntr   int | ||||
| } | ||||
|  | ||||
| func statusToErr(status *C.Status) error { | ||||
| 	return fmt.Errorf("(status = %d) %s", int(status.code), C.GoString(status.msg)) | ||||
| } | ||||
|  | ||||
| func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) { | ||||
| 	if params.KeyFrameInterval == 0 { | ||||
| 		params.KeyFrameInterval = 60 | ||||
| 	} | ||||
|  | ||||
| 	if params.BitRate == 0 { | ||||
| 		params.BitRate = 300000 | ||||
| 	} | ||||
|  | ||||
| 	e := encoder{ | ||||
| 		r: video.ToI420(r), | ||||
| 	} | ||||
| 	status := C.enc_new(C.Params{ | ||||
| 		width:              C.int(p.Width), | ||||
| 		height:             C.int(p.Height), | ||||
| 		bitrate:            C.uint(params.BitRate), | ||||
| 		key_frame_interval: C.uint(params.KeyFrameInterval), | ||||
| 	}, &e.engine) | ||||
| 	if status.code != 0 { | ||||
| 		return nil, statusToErr(&status) | ||||
| 	} | ||||
|  | ||||
| 	return &e, nil | ||||
| } | ||||
|  | ||||
| func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := e.r.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 	} | ||||
| 	imgReal := img.(*image.YCbCr) | ||||
| 	var y, cb, cr C.Slice | ||||
| 	y.data = (*C.uchar)(&imgReal.Y[0]) | ||||
| 	y.len = C.int(len(imgReal.Y)) | ||||
| 	cb.data = (*C.uchar)(&imgReal.Cb[0]) | ||||
| 	cb.len = C.int(len(imgReal.Cb)) | ||||
| 	cr.data = (*C.uchar)(&imgReal.Cr[0]) | ||||
| 	cr.len = C.int(len(imgReal.Cr)) | ||||
|  | ||||
| 	var encodedBuffer *C.MMAL_BUFFER_HEADER_T | ||||
| 	status := C.enc_encode(&e.engine, y, cb, cr, &encodedBuffer) | ||||
| 	if status.code != 0 { | ||||
| 		return nil, func() {}, statusToErr(&status) | ||||
| 	} | ||||
|  | ||||
| 	// GoBytes copies the C array to Go slice. After this, it's safe to release the C array | ||||
| 	encoded := C.GoBytes(unsafe.Pointer(encodedBuffer.data), C.int(encodedBuffer.length)) | ||||
| 	// Release the buffer so that mmal can reuse this memory | ||||
| 	C.mmal_buffer_header_release(encodedBuffer) | ||||
|  | ||||
| 	return encoded, func() {}, err | ||||
| } | ||||
|  | ||||
| func (e *encoder) SetBitRate(b int) error { | ||||
| 	panic("SetBitRate is not implemented") | ||||
| } | ||||
|  | ||||
| func (e *encoder) ForceKeyFrame() error { | ||||
| 	panic("ForceKeyFrame is not implemented") | ||||
| } | ||||
|  | ||||
| func (e *encoder) Close() error { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	e.closed = true | ||||
| 	C.enc_close(&e.engine) | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| package mmal | ||||
|  | ||||
| import ( | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
|  | ||||
| // Params stores libmmal specific encoding parameters. | ||||
| type Params struct { | ||||
| 	codec.BaseParams | ||||
| } | ||||
|  | ||||
| // NewParams returns default mmal codec specific parameters. | ||||
| func NewParams() (Params, error) { | ||||
| 	return Params{ | ||||
| 		BaseParams: codec.BaseParams{ | ||||
| 			KeyFrameInterval: 60, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPH264Codec(90000) | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds mmal encoder with given params | ||||
| func (p *Params) BuildVideoEncoder(r video.Reader, property prop.Media) (codec.ReadCloser, error) { | ||||
| 	return newEncoder(r, property, *p) | ||||
| } | ||||
| @@ -16,6 +16,7 @@ import ( | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	mio "github.com/pion/mediadevices/pkg/io" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
| @@ -23,6 +24,7 @@ import ( | ||||
| type encoder struct { | ||||
| 	engine *C.Encoder | ||||
| 	r      video.Reader | ||||
| 	buff   []byte | ||||
|  | ||||
| 	mu     sync.Mutex | ||||
| 	closed bool | ||||
| @@ -50,17 +52,26 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (e *encoder) Read() ([]byte, func(), error) { | ||||
| func (e *encoder) Read(p []byte) (n int, err error) { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := e.r.Read() | ||||
| 	if e.buff != nil { | ||||
| 		n, err = mio.Copy(p, e.buff) | ||||
| 		if err == nil { | ||||
| 			e.buff = nil | ||||
| 		} | ||||
|  | ||||
| 		return n, err | ||||
| 	} | ||||
|  | ||||
| 	img, err := e.r.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	yuvImg := img.(*image.YCbCr) | ||||
| @@ -74,11 +85,16 @@ func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 		width:  C.int(bounds.Max.X - bounds.Min.X), | ||||
| 	}, &rv) | ||||
| 	if err := errResult(rv); err != nil { | ||||
| 		return nil, func() {}, fmt.Errorf("failed in encoding: %v", err) | ||||
| 		return 0, fmt.Errorf("failed in encoding: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	encoded := C.GoBytes(unsafe.Pointer(s.data), s.data_len) | ||||
| 	return encoded, func() {}, nil | ||||
| 	n, err = mio.Copy(p, encoded) | ||||
| 	if err != nil { | ||||
| 		e.buff = encoded | ||||
| 	} | ||||
|  | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| func (e *encoder) SetBitRate(b int) error { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // Params stores libopenh264 specific encoding parameters. | ||||
| @@ -20,9 +21,9 @@ func NewParams() (Params, error) { | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPH264Codec(90000) | ||||
| // Name represents the codec name | ||||
| func (p *Params) Name() string { | ||||
| 	return webrtc.H264 | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds openh264 encoder with given params | ||||
|   | ||||
| @@ -72,22 +72,27 @@ func newEncoder(r audio.Reader, p prop.Media, params Params) (codec.ReadCloser, | ||||
| 	return &e, nil | ||||
| } | ||||
|  | ||||
| func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 	buff, _, err := e.reader.Read() | ||||
| func (e *encoder) Read(p []byte) (int, error) { | ||||
| 	buff, err := e.reader.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	encoded := make([]byte, 1024) | ||||
| 	switch b := buff.(type) { | ||||
| 	case *wave.Int16Interleaved: | ||||
| 		n, err := e.engine.Encode(b.Data, encoded) | ||||
| 		return encoded[:n:n], func() {}, err | ||||
| 		n, err := e.engine.Encode(b.Data, p) | ||||
| 		if err != nil { | ||||
| 			return n, err | ||||
| 		} | ||||
| 		return n, nil | ||||
| 	case *wave.Float32Interleaved: | ||||
| 		n, err := e.engine.EncodeFloat32(b.Data, encoded) | ||||
| 		return encoded[:n:n], func() {}, err | ||||
| 		n, err := e.engine.EncodeFloat32(b.Data, p) | ||||
| 		if err != nil { | ||||
| 			return n, err | ||||
| 		} | ||||
| 		return n, nil | ||||
| 	default: | ||||
| 		return nil, func() {}, errors.New("unknown type of audio buffer") | ||||
| 		return 0, errors.New("unknown type of audio buffer") | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/wave/mixer" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // Params stores opus specific encoding parameters. | ||||
| @@ -19,9 +20,9 @@ func NewParams() (Params, error) { | ||||
| 	return Params{}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPOpusCodec(48000) | ||||
| // Name represents the codec name | ||||
| func (p *Params) Name() string { | ||||
| 	return webrtc.Opus | ||||
| } | ||||
|  | ||||
| // BuildAudioEncoder builds opus encoder with given params | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // ParamsVP8 stores VP8 encoding parameters. | ||||
| @@ -43,9 +44,9 @@ func NewVP8Params() (ParamsVP8, error) { | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *ParamsVP8) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPVP8Codec(90000) | ||||
| // Name represents the codec name | ||||
| func (p *ParamsVP8) Name() string { | ||||
| 	return webrtc.VP8 | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds VP8 encoder with given params | ||||
| @@ -112,9 +113,9 @@ func NewVP9Params() (ParamsVP9, error) { | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *ParamsVP9) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPVP9Codec(90000) | ||||
| // Name represents the codec name | ||||
| func (p *ParamsVP9) Name() string { | ||||
| 	return webrtc.VP9 | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds VP9 encoder with given params | ||||
|   | ||||
| @@ -64,6 +64,7 @@ import ( | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	mio "github.com/pion/mediadevices/pkg/io" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
| @@ -79,6 +80,7 @@ const ( | ||||
|  | ||||
| type encoderVP8 struct { | ||||
| 	r     video.Reader | ||||
| 	buf   []byte | ||||
| 	frame []byte | ||||
|  | ||||
| 	fdDRI    C.int | ||||
| @@ -295,17 +297,25 @@ func newVP8Encoder(r video.Reader, p prop.Media, params ParamsVP8) (codec.ReadCl | ||||
| 	return e, nil | ||||
| } | ||||
|  | ||||
| func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| func (e *encoderVP8) Read(p []byte) (int, error) { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := e.r.Read() | ||||
| 	if e.buf != nil { | ||||
| 		n, err := mio.Copy(p, e.buf) | ||||
| 		if err == nil { | ||||
| 			e.buf = nil | ||||
| 		} | ||||
| 		return n, err | ||||
| 	} | ||||
|  | ||||
| 	img, err := e.r.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	yuvImg := img.(*image.YCbCr) | ||||
|  | ||||
| @@ -347,7 +357,7 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 			} | ||||
| 		} | ||||
| 		if e.picParam.reconstructed_frame == C.VA_INVALID_SURFACE { | ||||
| 			return nil, func() {}, errors.New("no available surface") | ||||
| 			return 0, errors.New("no available surface") | ||||
| 		} | ||||
|  | ||||
| 		C.setForceKFFlagVP8(&e.picParam, 0) | ||||
| @@ -415,7 +425,7 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 			C.size_t(uintptr(p.src)), | ||||
| 			&id, | ||||
| 		); s != C.VA_STATUS_SUCCESS { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to create buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 			return 0, fmt.Errorf("failed to create buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		} | ||||
| 		buffs = append(buffs, id) | ||||
| 	} | ||||
| @@ -425,17 +435,17 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 		e.display, e.ctxID, | ||||
| 		e.surfs[surfaceVP8Input], | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to begin picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to begin picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	// Upload image | ||||
| 	var vaImg C.VAImage | ||||
| 	var rawBuf unsafe.Pointer | ||||
| 	if s := C.vaDeriveImage(e.display, e.surfs[surfaceVP8Input], &vaImg); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to derive image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to derive image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if s := C.vaMapBuffer(e.display, vaImg.buf, &rawBuf); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	// TODO: use vaImg.pitches to support padding | ||||
| 	C.memcpy( | ||||
| @@ -451,10 +461,10 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 		unsafe.Pointer(&yuvImg.Cr[0]), C.size_t(len(yuvImg.Cr)), | ||||
| 	) | ||||
| 	if s := C.vaUnmapBuffer(e.display, vaImg.buf); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if s := C.vaDestroyImage(e.display, vaImg.image_id); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to destroy image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to destroy image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	if s := C.vaRenderPicture( | ||||
| @@ -462,38 +472,38 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 		&buffs[1], // 0 is for ouput | ||||
| 		C.int(len(buffs)-1), | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to render picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to render picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if s := C.vaEndPicture( | ||||
| 		e.display, e.ctxID, | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to end picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to end picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	// Load encoded data | ||||
| 	for retry := 3; retry >= 0; retry-- { | ||||
| 		if s := C.vaSyncSurface(e.display, e.picParam.reconstructed_frame); s != C.VA_STATUS_SUCCESS { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to sync surface: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 			return 0, fmt.Errorf("failed to sync surface: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		} | ||||
| 		var surfStat C.VASurfaceStatus | ||||
| 		if s := C.vaQuerySurfaceStatus( | ||||
| 			e.display, e.picParam.reconstructed_frame, &surfStat, | ||||
| 		); s != C.VA_STATUS_SUCCESS { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to query surface status: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 			return 0, fmt.Errorf("failed to query surface status: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		} | ||||
| 		if surfStat == C.VASurfaceReady { | ||||
| 			break | ||||
| 		} | ||||
| 		if retry == 0 { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to sync surface: %d", surfStat) | ||||
| 			return 0, fmt.Errorf("failed to sync surface: %d", surfStat) | ||||
| 		} | ||||
| 	} | ||||
| 	var seg *C.VACodedBufferSegment | ||||
| 	if s := C.vaMapBufferSeg(e.display, buffs[0], &seg); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if seg.status&C.VA_CODED_BUF_STATUS_SLICE_OVERFLOW_MASK != 0 { | ||||
| 		return nil, func() {}, errors.New("buffer size too small") | ||||
| 		return 0, errors.New("buffer size too small") | ||||
| 	} | ||||
|  | ||||
| 	if cap(e.frame) < int(seg.size) { | ||||
| @@ -506,13 +516,13 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 	) | ||||
|  | ||||
| 	if s := C.vaUnmapBuffer(e.display, buffs[0]); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	// Destroy buffers | ||||
| 	for _, b := range buffs { | ||||
| 		if s := C.vaDestroyBuffer(e.display, b); s != C.VA_STATUS_SUCCESS { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to destroy buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 			return 0, fmt.Errorf("failed to destroy buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -535,9 +545,11 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | ||||
| 	e.picParam.ref_last_frame = e.picParam.reconstructed_frame | ||||
| 	C.setRefreshLastFlagVP8(&e.picParam, 1) | ||||
|  | ||||
| 	encoded := make([]byte, len(e.frame)) | ||||
| 	copy(encoded, e.frame) | ||||
| 	return encoded, func() {}, err | ||||
| 	n, err := mio.Copy(p, e.frame) | ||||
| 	if err != nil { | ||||
| 		e.buf = e.frame | ||||
| 	} | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| func (e *encoderVP8) SetBitRate(b int) error { | ||||
|   | ||||
| @@ -47,6 +47,7 @@ import ( | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	mio "github.com/pion/mediadevices/pkg/io" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
| @@ -66,6 +67,7 @@ const ( | ||||
|  | ||||
| type encoderVP9 struct { | ||||
| 	r     video.Reader | ||||
| 	buf   []byte | ||||
| 	frame []byte | ||||
|  | ||||
| 	fdDRI    C.int | ||||
| @@ -284,17 +286,25 @@ func newVP9Encoder(r video.Reader, p prop.Media, params ParamsVP9) (codec.ReadCl | ||||
| 	return e, nil | ||||
| } | ||||
|  | ||||
| func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| func (e *encoderVP9) Read(p []byte) (int, error) { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := e.r.Read() | ||||
| 	if e.buf != nil { | ||||
| 		n, err := mio.Copy(p, e.buf) | ||||
| 		if err == nil { | ||||
| 			e.buf = nil | ||||
| 		} | ||||
| 		return n, err | ||||
| 	} | ||||
|  | ||||
| 	img, err := e.r.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	yuvImg := img.(*image.YCbCr) | ||||
|  | ||||
| @@ -378,7 +388,7 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| 			C.size_t(uintptr(p.src)), | ||||
| 			&id, | ||||
| 		); s != C.VA_STATUS_SUCCESS { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to create buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 			return 0, fmt.Errorf("failed to create buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		} | ||||
| 		buffs = append(buffs, id) | ||||
| 	} | ||||
| @@ -388,17 +398,17 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| 		e.display, e.ctxID, | ||||
| 		e.surfs[surfaceVP9Input], | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to begin picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to begin picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	// Upload image | ||||
| 	var vaImg C.VAImage | ||||
| 	var rawBuf unsafe.Pointer | ||||
| 	if s := C.vaDeriveImage(e.display, e.surfs[surfaceVP9Input], &vaImg); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to derive image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to derive image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if s := C.vaMapBuffer(e.display, vaImg.buf, &rawBuf); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	// TODO: use vaImg.pitches to support padding | ||||
| 	C.copyI420toNV12( | ||||
| @@ -409,10 +419,10 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| 		C.uint(len(yuvImg.Y)), | ||||
| 	) | ||||
| 	if s := C.vaUnmapBuffer(e.display, vaImg.buf); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if s := C.vaDestroyImage(e.display, vaImg.image_id); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to destroy image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to destroy image: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	if s := C.vaRenderPicture( | ||||
| @@ -420,27 +430,27 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| 		&buffs[1], // 0 is for ouput | ||||
| 		C.int(len(buffs)-1), | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to render picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to render picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if s := C.vaEndPicture( | ||||
| 		e.display, e.ctxID, | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to end picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to end picture: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	// Load encoded data | ||||
| 	if s := C.vaSyncSurface(e.display, e.picParam.reconstructed_frame); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to sync surface: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to sync surface: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	var surfStat C.VASurfaceStatus | ||||
| 	if s := C.vaQuerySurfaceStatus( | ||||
| 		e.display, e.picParam.reconstructed_frame, &surfStat, | ||||
| 	); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to query surface status: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to query surface status: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	var seg *C.VACodedBufferSegment | ||||
| 	if s := C.vaMapBufferSeg(e.display, buffs[0], &seg); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to map buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
| 	if cap(e.frame) < int(seg.size) { | ||||
| 		e.frame = make([]byte, int(seg.size)) | ||||
| @@ -452,13 +462,13 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| 	) | ||||
|  | ||||
| 	if s := C.vaUnmapBuffer(e.display, buffs[0]); s != C.VA_STATUS_SUCCESS { | ||||
| 		return nil, func() {}, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		return 0, fmt.Errorf("failed to unmap buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 	} | ||||
|  | ||||
| 	// Destroy buffers | ||||
| 	for _, b := range buffs { | ||||
| 		if s := C.vaDestroyBuffer(e.display, b); s != C.VA_STATUS_SUCCESS { | ||||
| 			return nil, func() {}, fmt.Errorf("failed to destroy buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 			return 0, fmt.Errorf("failed to destroy buffer: %s", C.GoString(C.vaErrorStr(s))) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -470,9 +480,11 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | ||||
| 		e.slotCurr = 0 | ||||
| 	} | ||||
|  | ||||
| 	encoded := make([]byte, len(e.frame)) | ||||
| 	copy(encoded, e.frame) | ||||
| 	return encoded, func() {}, err | ||||
| 	n, err := mio.Copy(p, e.frame) | ||||
| 	if err != nil { | ||||
| 		e.buf = e.frame | ||||
| 	} | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| func (e *encoderVP9) SetBitRate(b int) error { | ||||
|   | ||||
| @@ -56,8 +56,10 @@ import ( | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	mio "github.com/pion/mediadevices/pkg/io" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| type encoder struct { | ||||
| @@ -66,6 +68,7 @@ type encoder struct { | ||||
| 	cfg        *C.vpx_codec_enc_cfg_t | ||||
| 	r          video.Reader | ||||
| 	frameIndex int | ||||
| 	buff       []byte | ||||
| 	tStart     int | ||||
| 	tLastFrame int | ||||
| 	frame      []byte | ||||
| @@ -92,9 +95,9 @@ func NewVP8Params() (VP8Params, error) { | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *VP8Params) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPVP8Codec(90000) | ||||
| // Name represents the codec name | ||||
| func (p *VP8Params) Name() string { | ||||
| 	return webrtc.VP8 | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds VP8 encoder with given params | ||||
| @@ -119,9 +122,9 @@ func NewVP9Params() (VP9Params, error) { | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *VP9Params) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPVP9Codec(90000) | ||||
| // Name represents the codec name | ||||
| func (p *VP9Params) Name() string { | ||||
| 	return webrtc.VP9 | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds VP9 encoder with given params | ||||
| @@ -204,17 +207,25 @@ func newEncoder(r video.Reader, p prop.Media, params Params, codecIface *C.vpx_c | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (e *encoder) Read() ([]byte, func(), error) { | ||||
| func (e *encoder) Read(p []byte) (int, error) { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := e.r.Read() | ||||
| 	if e.buff != nil { | ||||
| 		n, err := mio.Copy(p, e.buff) | ||||
| 		if err == nil { | ||||
| 			e.buff = nil | ||||
| 		} | ||||
| 		return n, err | ||||
| 	} | ||||
|  | ||||
| 	img, err := e.r.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	yuvImg := img.(*image.YCbCr) | ||||
| 	bounds := yuvImg.Bounds() | ||||
| @@ -230,7 +241,7 @@ func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 	if e.cfg.g_w != C.uint(width) || e.cfg.g_h != C.uint(height) { | ||||
| 		e.cfg.g_w, e.cfg.g_h = C.uint(width), C.uint(height) | ||||
| 		if ec := C.vpx_codec_enc_config_set(e.codec, e.cfg); ec != C.VPX_CODEC_OK { | ||||
| 			return nil, func() {}, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", ec) | ||||
| 			return 0, fmt.Errorf("vpx_codec_enc_config_set failed (%d)", ec) | ||||
| 		} | ||||
| 		e.raw.w, e.raw.h = C.uint(width), C.uint(height) | ||||
| 		e.raw.r_w, e.raw.r_h = C.uint(width), C.uint(height) | ||||
| @@ -243,7 +254,7 @@ func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 		C.long(t-e.tStart), C.ulong(t-e.tLastFrame), C.long(flags), C.ulong(e.deadline), | ||||
| 		(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]), | ||||
| 	); ec != C.VPX_CODEC_OK { | ||||
| 		return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec) | ||||
| 		return 0, fmt.Errorf("vpx_codec_encode failed (%d)", ec) | ||||
| 	} | ||||
|  | ||||
| 	e.frameIndex++ | ||||
| @@ -261,10 +272,11 @@ func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 			e.frame = append(e.frame, encoded...) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	encoded := make([]byte, len(e.frame)) | ||||
| 	copy(encoded, e.frame) | ||||
| 	return encoded, func() {}, err | ||||
| 	n, err := mio.Copy(p, e.frame) | ||||
| 	if err != nil { | ||||
| 		e.buff = e.frame | ||||
| 	} | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| func (e *encoder) SetBitRate(b int) error { | ||||
|   | ||||
| @@ -47,7 +47,7 @@ Encoder *enc_new(x264_param_t param, char *preset, int *rc) { | ||||
|   e->param.b_repeat_headers = 1; | ||||
|   e->param.b_annexb = 1; | ||||
|  | ||||
|   if (x264_param_apply_profile(&e->param, "high") < 0) { | ||||
|   if (x264_param_apply_profile(&e->param, "baseline") < 0) { | ||||
|     *rc = ERR_APPLY_PROFILE; | ||||
|     goto fail; | ||||
|   } | ||||
| @@ -95,4 +95,4 @@ void enc_close(Encoder *e, int *rc) { | ||||
|   x264_encoder_close(e->h); | ||||
|   x264_picture_clean(&e->pic_in); | ||||
|   free(e); | ||||
| } | ||||
| } | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // Params stores libx264 specific encoding parameters. | ||||
| @@ -39,9 +40,9 @@ func NewParams() (Params, error) { | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // RTPCodec represents the codec metadata | ||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | ||||
| 	return codec.NewRTPH264Codec(90000) | ||||
| // Name represents the codec name | ||||
| func (p *Params) Name() string { | ||||
| 	return webrtc.H264 | ||||
| } | ||||
|  | ||||
| // BuildVideoEncoder builds x264 encoder with given params | ||||
|   | ||||
| @@ -14,12 +14,14 @@ import ( | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	mio "github.com/pion/mediadevices/pkg/io" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| ) | ||||
|  | ||||
| type encoder struct { | ||||
| 	engine *C.Encoder | ||||
| 	buff   []byte | ||||
| 	r      video.Reader | ||||
| 	mu     sync.Mutex | ||||
| 	closed bool | ||||
| @@ -94,17 +96,25 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, | ||||
| 	return &e, nil | ||||
| } | ||||
|  | ||||
| func (e *encoder) Read() ([]byte, func(), error) { | ||||
| func (e *encoder) Read(p []byte) (int, error) { | ||||
| 	e.mu.Lock() | ||||
| 	defer e.mu.Unlock() | ||||
|  | ||||
| 	if e.closed { | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return 0, io.EOF | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := e.r.Read() | ||||
| 	if e.buff != nil { | ||||
| 		n, err := mio.Copy(p, e.buff) | ||||
| 		if err == nil { | ||||
| 			e.buff = nil | ||||
| 		} | ||||
| 		return n, err | ||||
| 	} | ||||
|  | ||||
| 	img, err := e.r.Read() | ||||
| 	if err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	yuvImg := img.(*image.YCbCr) | ||||
|  | ||||
| @@ -117,11 +127,15 @@ func (e *encoder) Read() ([]byte, func(), error) { | ||||
| 		&rc, | ||||
| 	) | ||||
| 	if err := errFromC(rc); err != nil { | ||||
| 		return nil, func() {}, err | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	encoded := C.GoBytes(unsafe.Pointer(s.data), s.data_len) | ||||
| 	return encoded, func() {}, err | ||||
| 	n, err := mio.Copy(p, encoded) | ||||
| 	if err != nil { | ||||
| 		e.buff = encoded | ||||
| 	} | ||||
| 	return n, err | ||||
| } | ||||
|  | ||||
| func (e *encoder) SetBitRate(b int) error { | ||||
|   | ||||
| @@ -52,10 +52,10 @@ func (d *dummy) AudioRecord(p prop.Media) (audio.Reader, error) { | ||||
|  | ||||
| 	closed := d.closed | ||||
|  | ||||
| 	reader := audio.ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 	reader := audio.ReaderFunc(func() (wave.Audio, error) { | ||||
| 		select { | ||||
| 		case <-closed: | ||||
| 			return nil, func() {}, io.EOF | ||||
| 			return nil, io.EOF | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| @@ -78,7 +78,7 @@ func (d *dummy) AudioRecord(p prop.Media) (audio.Reader, error) { | ||||
| 				a.SetFloat32(i, ch, wave.Float32Sample(sin[phase])) | ||||
| 			} | ||||
| 		} | ||||
| 		return a, func() {}, nil | ||||
| 		return a, nil | ||||
| 	}) | ||||
| 	return reader, nil | ||||
| } | ||||
|   | ||||
| @@ -56,10 +56,10 @@ func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) { | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		frame, _, err := rc.Read() | ||||
| 	r := video.ReaderFunc(func() (image.Image, error) { | ||||
| 		frame, err := rc.Read() | ||||
| 		if err != nil { | ||||
| 			return nil, func() {}, err | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return decoder.Decode(frame, property.Width, property.Height) | ||||
| 	}) | ||||
|   | ||||
| @@ -8,8 +8,7 @@ import ( | ||||
| 	"errors" | ||||
| 	"image" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"io/ioutil" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/blackjack/webcam" | ||||
| @@ -26,36 +25,6 @@ const ( | ||||
| var ( | ||||
| 	errReadTimeout = errors.New("read timeout") | ||||
| 	errEmptyFrame  = errors.New("empty frame") | ||||
| 	// Reference: https://commons.wikimedia.org/wiki/File:Vector_Video_Standards2.svg | ||||
| 	supportedResolutions = [][2]int{ | ||||
| 		{320, 240}, | ||||
| 		{640, 480}, | ||||
| 		{768, 576}, | ||||
| 		{800, 600}, | ||||
| 		{1024, 768}, | ||||
| 		{1280, 854}, | ||||
| 		{1280, 960}, | ||||
| 		{1280, 1024}, | ||||
| 		{1400, 1050}, | ||||
| 		{1600, 1200}, | ||||
| 		{2048, 1536}, | ||||
| 		{320, 200}, | ||||
| 		{800, 480}, | ||||
| 		{854, 480}, | ||||
| 		{1024, 600}, | ||||
| 		{1152, 768}, | ||||
| 		{1280, 720}, | ||||
| 		{1280, 768}, | ||||
| 		{1366, 768}, | ||||
| 		{1280, 800}, | ||||
| 		{1440, 900}, | ||||
| 		{1440, 960}, | ||||
| 		{1680, 1050}, | ||||
| 		{1920, 1080}, | ||||
| 		{2048, 1080}, | ||||
| 		{1920, 1200}, | ||||
| 		{2560, 1600}, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // Camera implementation using v4l2 | ||||
| @@ -71,47 +40,27 @@ type camera struct { | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	discovered := make(map[string]struct{}) | ||||
|  | ||||
| 	discover := func(pattern string) { | ||||
| 		devices, err := filepath.Glob(pattern) | ||||
| 		if err != nil { | ||||
| 			// No v4l device. | ||||
| 			return | ||||
| 		} | ||||
| 		for _, device := range devices { | ||||
| 			label := filepath.Base(device) | ||||
| 			reallink, err := os.Readlink(device) | ||||
| 			if err != nil { | ||||
| 				reallink = label | ||||
| 			} else { | ||||
| 				reallink = filepath.Base(reallink) | ||||
| 			} | ||||
|  | ||||
| 			if _, ok := discovered[reallink]; ok { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			discovered[reallink] = struct{}{} | ||||
| 			cam := newCamera(device) | ||||
| 			driver.GetManager().Register(cam, driver.Info{ | ||||
| 				Label:      label, | ||||
| 				DeviceType: driver.Camera, | ||||
| 			}) | ||||
| 		} | ||||
| 	searchPath := "/dev/v4l/by-path/" | ||||
| 	devices, err := ioutil.ReadDir(searchPath) | ||||
| 	if err != nil { | ||||
| 		// No v4l device. | ||||
| 		return | ||||
| 	} | ||||
| 	for _, device := range devices { | ||||
| 		cam := newCamera(searchPath + device.Name()) | ||||
| 		driver.GetManager().Register(cam, driver.Info{ | ||||
| 			Label:      device.Name(), | ||||
| 			DeviceType: driver.Camera, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	discover("/dev/v4l/by-path/*") | ||||
| 	discover("/dev/video*") | ||||
| } | ||||
|  | ||||
| func newCamera(path string) *camera { | ||||
| 	formats := map[webcam.PixelFormat]frame.Format{ | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_YUV420): frame.FormatI420, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV):   frame.FormatYUYV, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY):   frame.FormatUYVY, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_NV12):   frame.FormatNV21, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG):  frame.FormatMJPEG, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_YUYV):  frame.FormatYUYV, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY):  frame.FormatUYVY, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_NV12):  frame.FormatNV21, | ||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG, | ||||
| 	} | ||||
|  | ||||
| 	reversedFormats := make(map[frame.Format]webcam.PixelFormat) | ||||
| @@ -133,8 +82,6 @@ func (c *camera) Open() error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Late frames should be discarded. Buffering should be handled in higher level. | ||||
| 	cam.SetBufferCount(1) | ||||
| 	c.cam = cam | ||||
| 	return nil | ||||
| } | ||||
| @@ -182,7 +129,7 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	c.cancel = cancel | ||||
| 	var buf []byte | ||||
| 	r := video.ReaderFunc(func() (img image.Image, release func(), err error) { | ||||
| 	r := video.ReaderFunc(func() (img image.Image, err error) { | ||||
| 		// Lock to avoid accessing the buffer after StopStreaming() | ||||
| 		c.mutex.Lock() | ||||
| 		defer c.mutex.Unlock() | ||||
| @@ -191,23 +138,23 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 		for i := 0; i < maxEmptyFrameCount; i++ { | ||||
| 			if ctx.Err() != nil { | ||||
| 				// Return EOF if the camera is already closed. | ||||
| 				return nil, func() {}, io.EOF | ||||
| 				return nil, io.EOF | ||||
| 			} | ||||
|  | ||||
| 			err := cam.WaitForFrame(5) // 5 seconds | ||||
| 			switch err.(type) { | ||||
| 			case nil: | ||||
| 			case *webcam.Timeout: | ||||
| 				return nil, func() {}, errReadTimeout | ||||
| 				return nil, errReadTimeout | ||||
| 			default: | ||||
| 				// Camera has been stopped. | ||||
| 				return nil, func() {}, err | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			b, err := cam.ReadFrame() | ||||
| 			if err != nil { | ||||
| 				// Camera has been stopped. | ||||
| 				return nil, func() {}, err | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			// Frame is empty. | ||||
| @@ -227,7 +174,7 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 			n := copy(buf, b) | ||||
| 			return decoder.Decode(buf[:n], p.Width, p.Height) | ||||
| 		} | ||||
| 		return nil, func() {}, errEmptyFrame | ||||
| 		return nil, errEmptyFrame | ||||
| 	}) | ||||
|  | ||||
| 	return r, nil | ||||
| @@ -237,46 +184,13 @@ func (c *camera) Properties() []prop.Media { | ||||
| 	properties := make([]prop.Media, 0) | ||||
| 	for format := range c.cam.GetSupportedFormats() { | ||||
| 		for _, frameSize := range c.cam.GetSupportedFrameSizes(format) { | ||||
| 			supportedFormat, ok := c.formats[format] | ||||
| 			if !ok { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if frameSize.StepWidth == 0 || frameSize.StepHeight == 0 { | ||||
| 				properties = append(properties, prop.Media{ | ||||
| 					Video: prop.Video{ | ||||
| 						Width:       int(frameSize.MaxWidth), | ||||
| 						Height:      int(frameSize.MaxHeight), | ||||
| 						FrameFormat: supportedFormat, | ||||
| 					}, | ||||
| 				}) | ||||
| 			} else { | ||||
| 				// FIXME: we should probably use a custom data structure to capture all of the supported resolutions | ||||
| 				for _, supportedResolution := range supportedResolutions { | ||||
| 					minWidth, minHeight := int(frameSize.MinWidth), int(frameSize.MinHeight) | ||||
| 					maxWidth, maxHeight := int(frameSize.MaxWidth), int(frameSize.MaxHeight) | ||||
| 					stepWidth, stepHeight := int(frameSize.StepWidth), int(frameSize.StepHeight) | ||||
| 					width, height := supportedResolution[0], supportedResolution[1] | ||||
|  | ||||
| 					if width < minWidth || width > maxWidth || | ||||
| 						height < minHeight || height > maxHeight { | ||||
| 						continue | ||||
| 					} | ||||
|  | ||||
| 					if (width-minWidth)%stepWidth != 0 || | ||||
| 						(height-minHeight)%stepHeight != 0 { | ||||
| 						continue | ||||
| 					} | ||||
|  | ||||
| 					properties = append(properties, prop.Media{ | ||||
| 						Video: prop.Video{ | ||||
| 							Width:       width, | ||||
| 							Height:      height, | ||||
| 							FrameFormat: supportedFormat, | ||||
| 						}, | ||||
| 					}) | ||||
| 				} | ||||
| 			} | ||||
| 			properties = append(properties, prop.Media{ | ||||
| 				Video: prop.Video{ | ||||
| 					Width:       int(frameSize.MaxWidth), | ||||
| 					Height:      int(frameSize.MaxHeight), | ||||
| 					FrameFormat: c.formats[format], | ||||
| 				}, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| 	return properties | ||||
|   | ||||
| @@ -116,10 +116,10 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
|  | ||||
| 	img := &image.YCbCr{} | ||||
|  | ||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | ||||
| 	r := video.ReaderFunc(func() (image.Image, error) { | ||||
| 		b, ok := <-c.ch | ||||
| 		if !ok { | ||||
| 			return nil, func() {}, io.EOF | ||||
| 			return nil, io.EOF | ||||
| 		} | ||||
| 		img.Y = b[:nPix] | ||||
| 		img.Cb = b[nPix : nPix+nPix/2] | ||||
| @@ -128,7 +128,7 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 		img.CStride = p.Width / 2 | ||||
| 		img.SubsampleRatio = image.YCbCrSubsampleRatio422 | ||||
| 		img.Rect = image.Rect(0, 0, p.Width, p.Height) | ||||
| 		return img, func() {}, nil | ||||
| 		return img, nil | ||||
| 	}) | ||||
| 	return r, nil | ||||
| } | ||||
|   | ||||
| @@ -1,204 +1 @@ | ||||
| package microphone | ||||
|  | ||||
| import ( | ||||
| 	"encoding/binary" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"time" | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/gen2brain/malgo" | ||||
| 	"github.com/pion/mediadevices/internal/logging" | ||||
| 	"github.com/pion/mediadevices/pkg/driver" | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	maxDeviceIDLength = 20 | ||||
| 	// TODO: should replace this with a more flexible approach | ||||
| 	sampleRateStep    = 1000 | ||||
| 	initialBufferSize = 1024 | ||||
| ) | ||||
|  | ||||
| var logger = logging.NewLogger("mediadevices/driver/microphone") | ||||
| var ctx *malgo.AllocatedContext | ||||
| var hostEndian binary.ByteOrder | ||||
| var ( | ||||
| 	errUnsupportedFormat = errors.New("the provided audio format is not supported") | ||||
| ) | ||||
|  | ||||
| type microphone struct { | ||||
| 	malgo.DeviceInfo | ||||
| 	chunkChan chan []byte | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	var err error | ||||
| 	/* | ||||
| 		backends := []malgo.Backend{ | ||||
| 			malgo.BackendPulseaudio, | ||||
| 			malgo.BackendAlsa, | ||||
| 		} | ||||
| 	*/ | ||||
| 	ctx, err = malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) { | ||||
| 		logger.Debugf("%v\n", message) | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	devices, err := ctx.Devices(malgo.Capture) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	for _, device := range devices { | ||||
| 		// TODO: Detect default device and prioritize it | ||||
| 		driver.GetManager().Register(newMicrophone(device), driver.Info{ | ||||
| 			Label:      device.ID.String(), | ||||
| 			DeviceType: driver.Microphone, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	// Decide which endian | ||||
| 	switch v := *(*uint16)(unsafe.Pointer(&([]byte{0x12, 0x34}[0]))); v { | ||||
| 	case 0x1234: | ||||
| 		hostEndian = binary.BigEndian | ||||
| 	case 0x3412: | ||||
| 		hostEndian = binary.LittleEndian | ||||
| 	default: | ||||
| 		panic(fmt.Sprintf("failed to determine host endianness: %x", v)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newMicrophone(info malgo.DeviceInfo) *microphone { | ||||
| 	return µphone{ | ||||
| 		DeviceInfo: info, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *microphone) Open() error { | ||||
| 	m.chunkChan = make(chan []byte, 1) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) Close() error { | ||||
| 	if m.chunkChan != nil { | ||||
| 		close(m.chunkChan) | ||||
| 		m.chunkChan = nil | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) { | ||||
| 	var config malgo.DeviceConfig | ||||
| 	var callbacks malgo.DeviceCallbacks | ||||
|  | ||||
| 	decoder, err := wave.NewDecoder(&wave.RawFormat{ | ||||
| 		SampleSize:  inputProp.SampleSize, | ||||
| 		IsFloat:     inputProp.IsFloat, | ||||
| 		Interleaved: inputProp.IsInterleaved, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	config.DeviceType = malgo.Capture | ||||
| 	config.PerformanceProfile = malgo.LowLatency | ||||
| 	config.Capture.Channels = uint32(inputProp.ChannelCount) | ||||
| 	config.SampleRate = uint32(inputProp.SampleRate) | ||||
| 	if inputProp.SampleSize == 4 && inputProp.IsFloat { | ||||
| 		config.Capture.Format = malgo.FormatF32 | ||||
| 	} else if inputProp.SampleSize == 2 && !inputProp.IsFloat { | ||||
| 		config.Capture.Format = malgo.FormatS16 | ||||
| 	} else { | ||||
| 		return nil, errUnsupportedFormat | ||||
| 	} | ||||
|  | ||||
| 	onRecvChunk := func(_, chunk []byte, framecount uint32) { | ||||
| 		m.chunkChan <- chunk | ||||
| 	} | ||||
| 	callbacks.Data = onRecvChunk | ||||
|  | ||||
| 	device, err := malgo.InitDevice(ctx.Context, config, callbacks) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	err = device.Start() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return audio.ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 		chunk, ok := <-m.chunkChan | ||||
| 		if !ok { | ||||
| 			device.Stop() | ||||
| 			device.Uninit() | ||||
| 			return nil, func() {}, io.EOF | ||||
| 		} | ||||
|  | ||||
| 		decodedChunk, err := decoder.Decode(hostEndian, chunk, inputProp.ChannelCount) | ||||
| 		// FIXME: the decoder should also fill this information | ||||
| 		decodedChunk.(*wave.Float32Interleaved).Size.SamplingRate = inputProp.SampleRate | ||||
| 		return decodedChunk, func() {}, err | ||||
| 	}), nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) Properties() []prop.Media { | ||||
| 	var supportedProps []prop.Media | ||||
| 	logger.Debug("Querying properties") | ||||
|  | ||||
| 	var isBigEndian bool | ||||
| 	// miniaudio only uses the host endian | ||||
| 	if hostEndian == binary.BigEndian { | ||||
| 		isBigEndian = true | ||||
| 	} | ||||
|  | ||||
| 	for ch := m.MinChannels; ch <= m.MaxChannels; ch++ { | ||||
| 		for sampleRate := m.MinSampleRate; sampleRate <= m.MaxSampleRate; sampleRate += sampleRateStep { | ||||
| 			for i := 0; i < int(m.FormatCount); i++ { | ||||
| 				format := m.Formats[i] | ||||
|  | ||||
| 				supportedProp := prop.Media{ | ||||
| 					Audio: prop.Audio{ | ||||
| 						ChannelCount: int(ch), | ||||
| 						SampleRate:   int(sampleRate), | ||||
| 						IsBigEndian:  isBigEndian, | ||||
| 						// miniaudio only supports interleaved at the moment | ||||
| 						IsInterleaved: true, | ||||
| 					}, | ||||
| 				} | ||||
|  | ||||
| 				switch malgo.FormatType(format) { | ||||
| 				case malgo.FormatF32: | ||||
| 					supportedProp.SampleSize = 4 | ||||
| 					supportedProp.IsFloat = true | ||||
| 				case malgo.FormatS16: | ||||
| 					supportedProp.SampleSize = 2 | ||||
| 					supportedProp.IsFloat = false | ||||
| 				} | ||||
|  | ||||
| 				supportedProps = append(supportedProps, supportedProp) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// FIXME: remove this hardcoded value. Malgo doesn't support "ma_context_get_device_info" API yet. The above iterations | ||||
| 	//        will always return nothing as of now | ||||
| 	supportedProps = append(supportedProps, prop.Media{ | ||||
| 		Audio: prop.Audio{ | ||||
| 			Latency:       time.Millisecond * 20, | ||||
| 			ChannelCount:  1, | ||||
| 			SampleRate:    48000, | ||||
| 			SampleSize:    4, | ||||
| 			IsFloat:       true, | ||||
| 			IsBigEndian:   isBigEndian, | ||||
| 			IsInterleaved: true, | ||||
| 		}, | ||||
| 	}) | ||||
| 	return supportedProps | ||||
| } | ||||
|   | ||||
							
								
								
									
										137
									
								
								pkg/driver/microphone/microphone_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								pkg/driver/microphone/microphone_linux.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| package microphone | ||||
|  | ||||
| import ( | ||||
| 	"io" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/jfreymuth/pulse" | ||||
| 	"github.com/pion/mediadevices/pkg/driver" | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| type microphone struct { | ||||
| 	c           *pulse.Client | ||||
| 	id          string | ||||
| 	samplesChan chan<- []int16 | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	pa, err := pulse.NewClient() | ||||
| 	if err != nil { | ||||
| 		// No pulseaudio | ||||
| 		return | ||||
| 	} | ||||
| 	defer pa.Close() | ||||
| 	sources, err := pa.ListSources() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defaultSource, err := pa.DefaultSource() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	for _, source := range sources { | ||||
| 		priority := driver.PriorityNormal | ||||
| 		if defaultSource.ID() == source.ID() { | ||||
| 			priority = driver.PriorityHigh | ||||
| 		} | ||||
| 		driver.GetManager().Register(µphone{id: source.ID()}, driver.Info{ | ||||
| 			Label:      source.ID(), | ||||
| 			DeviceType: driver.Microphone, | ||||
| 			Priority:   priority, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *microphone) Open() error { | ||||
| 	var err error | ||||
| 	m.c, err = pulse.NewClient() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) Close() error { | ||||
| 	if m.samplesChan != nil { | ||||
| 		close(m.samplesChan) | ||||
| 		m.samplesChan = nil | ||||
| 	} | ||||
|  | ||||
| 	m.c.Close() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) { | ||||
| 	var options []pulse.RecordOption | ||||
| 	if p.ChannelCount == 1 { | ||||
| 		options = append(options, pulse.RecordMono) | ||||
| 	} else { | ||||
| 		options = append(options, pulse.RecordStereo) | ||||
| 	} | ||||
| 	latency := p.Latency.Seconds() | ||||
|  | ||||
| 	src, err := m.c.SourceByID(m.id) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	options = append(options, | ||||
| 		pulse.RecordSampleRate(p.SampleRate), | ||||
| 		pulse.RecordLatency(latency), | ||||
| 		pulse.RecordSource(src), | ||||
| 	) | ||||
|  | ||||
| 	samplesChan := make(chan []int16, 1) | ||||
|  | ||||
| 	handler := func(b []int16) (int, error) { | ||||
| 		samplesChan <- b | ||||
| 		return len(b), nil | ||||
| 	} | ||||
|  | ||||
| 	stream, err := m.c.NewRecord(pulse.Int16Writer(handler), options...) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	reader := audio.ReaderFunc(func() (wave.Audio, error) { | ||||
| 		buff, ok := <-samplesChan | ||||
| 		if !ok { | ||||
| 			stream.Close() | ||||
| 			return nil, io.EOF | ||||
| 		} | ||||
|  | ||||
| 		a := wave.NewInt16Interleaved( | ||||
| 			wave.ChunkInfo{ | ||||
| 				Channels: p.ChannelCount, | ||||
| 				Len:      len(buff) / p.ChannelCount, | ||||
| 			}, | ||||
| 		) | ||||
| 		copy(a.Data, buff) | ||||
|  | ||||
| 		return a, nil | ||||
| 	}) | ||||
|  | ||||
| 	stream.Start() | ||||
| 	m.samplesChan = samplesChan | ||||
| 	return reader, nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) Properties() []prop.Media { | ||||
| 	// TODO: Get actual properties | ||||
| 	monoProp := prop.Media{ | ||||
| 		Audio: prop.Audio{ | ||||
| 			SampleRate:   48000, | ||||
| 			Latency:      time.Millisecond * 20, | ||||
| 			ChannelCount: 1, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	stereoProp := monoProp | ||||
| 	stereoProp.ChannelCount = 2 | ||||
|  | ||||
| 	return []prop.Media{monoProp, stereoProp} | ||||
| } | ||||
							
								
								
									
										347
									
								
								pkg/driver/microphone/microphone_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								pkg/driver/microphone/microphone_windows.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,347 @@ | ||||
| package microphone | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"golang.org/x/sys/windows" | ||||
| 	"io" | ||||
| 	"time" | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/driver" | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// bufferNumber * prop.Audio.Latency is the maximum blockable duration | ||||
| 	// to get data without dropping chunks. | ||||
| 	bufferNumber = 5 | ||||
| ) | ||||
|  | ||||
| // Windows APIs | ||||
| var ( | ||||
| 	winmm                 = windows.NewLazySystemDLL("Winmm.dll") | ||||
| 	waveInOpen            = winmm.NewProc("waveInOpen") | ||||
| 	waveInStart           = winmm.NewProc("waveInStart") | ||||
| 	waveInStop            = winmm.NewProc("waveInStop") | ||||
| 	waveInReset           = winmm.NewProc("waveInReset") | ||||
| 	waveInClose           = winmm.NewProc("waveInClose") | ||||
| 	waveInPrepareHeader   = winmm.NewProc("waveInPrepareHeader") | ||||
| 	waveInAddBuffer       = winmm.NewProc("waveInAddBuffer") | ||||
| 	waveInUnprepareHeader = winmm.NewProc("waveInUnprepareHeader") | ||||
| ) | ||||
|  | ||||
| type buffer struct { | ||||
| 	waveHdr | ||||
| 	data []int16 | ||||
| } | ||||
|  | ||||
| func newBuffer(samples int) *buffer { | ||||
| 	b := make([]int16, samples) | ||||
| 	return &buffer{ | ||||
| 		waveHdr: waveHdr{ | ||||
| 			// Sharing Go memory with Windows C API without reference. | ||||
| 			// Make sure that the lifetime of the buffer struct is longer | ||||
| 			// than the final access from cbWaveIn. | ||||
| 			lpData:         uintptr(unsafe.Pointer(&b[0])), | ||||
| 			dwBufferLength: uint32(samples * 2), | ||||
| 		}, | ||||
| 		data: b, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type microphone struct { | ||||
| 	hWaveIn windows.Pointer | ||||
| 	buf     map[uintptr]*buffer | ||||
| 	chBuf   chan *buffer | ||||
| 	closed  chan struct{} | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	// TODO: enum devices | ||||
| 	driver.GetManager().Register(µphone{}, driver.Info{ | ||||
| 		Label:      "default", | ||||
| 		DeviceType: driver.Microphone, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (m *microphone) Open() error { | ||||
| 	m.chBuf = make(chan *buffer) | ||||
| 	m.buf = make(map[uintptr]*buffer) | ||||
| 	m.closed = make(chan struct{}) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) cbWaveIn(hWaveIn windows.Pointer, uMsg uint, dwInstance, dwParam1, dwParam2 *int32) uintptr { | ||||
| 	switch uMsg { | ||||
| 	case MM_WIM_DATA: | ||||
| 		b := m.buf[uintptr(unsafe.Pointer(dwParam1))] | ||||
| 		m.chBuf <- b | ||||
|  | ||||
| 	case MM_WIM_OPEN: | ||||
| 	case MM_WIM_CLOSE: | ||||
| 		close(m.chBuf) | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
|  | ||||
| func (m *microphone) Close() error { | ||||
| 	if m.hWaveIn == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	close(m.closed) | ||||
|  | ||||
| 	ret, _, _ := waveInStop.Call( | ||||
| 		uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 	) | ||||
| 	if err := errWinmm[ret]; err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	// All enqueued buffers are marked done by waveInReset. | ||||
| 	ret, _, _ = waveInReset.Call( | ||||
| 		uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 	) | ||||
| 	if err := errWinmm[ret]; err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, buf := range m.buf { | ||||
| 		// Detach buffers from waveIn API. | ||||
| 		ret, _, _ := waveInUnprepareHeader.Call( | ||||
| 			uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 			uintptr(unsafe.Pointer(&buf.waveHdr)), | ||||
| 			uintptr(unsafe.Sizeof(buf.waveHdr)), | ||||
| 		) | ||||
| 		if err := errWinmm[ret]; err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	// Now, it's ready to free the buffers. | ||||
| 	// As microphone struct still has reference to the buffers, | ||||
| 	// they will be GC-ed once microphone is reopened or unreferenced. | ||||
|  | ||||
| 	ret, _, _ = waveInClose.Call( | ||||
| 		uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 	) | ||||
| 	if err := errWinmm[ret]; err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	<-m.chBuf | ||||
| 	m.hWaveIn = nil | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) AudioRecord(p prop.Media) (audio.Reader, error) { | ||||
| 	for i := 0; i < bufferNumber; i++ { | ||||
| 		b := newBuffer( | ||||
| 			int(uint64(p.Latency) * uint64(p.SampleRate) / uint64(time.Second)), | ||||
| 		) | ||||
| 		// Map the buffer by its data head address to restore access to the Go struct | ||||
| 		// in callback function. Don't resize the buffer after it. | ||||
| 		m.buf[uintptr(unsafe.Pointer(&b.waveHdr))] = b | ||||
| 	} | ||||
|  | ||||
| 	waveFmt := &waveFormatEx{ | ||||
| 		wFormatTag:      WAVE_FORMAT_PCM, | ||||
| 		nChannels:       uint16(p.ChannelCount), | ||||
| 		nSamplesPerSec:  uint32(p.SampleRate), | ||||
| 		nAvgBytesPerSec: uint32(p.SampleRate * p.ChannelCount * 2), | ||||
| 		nBlockAlign:     uint16(p.ChannelCount * 2), | ||||
| 		wBitsPerSample:  16, | ||||
| 	} | ||||
| 	ret, _, _ := waveInOpen.Call( | ||||
| 		uintptr(unsafe.Pointer(&m.hWaveIn)), | ||||
| 		WAVE_MAPPER, | ||||
| 		uintptr(unsafe.Pointer(waveFmt)), | ||||
| 		windows.NewCallback(m.cbWaveIn), | ||||
| 		0, | ||||
| 		CALLBACK_FUNCTION, | ||||
| 	) | ||||
| 	if err := errWinmm[ret]; err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for _, buf := range m.buf { | ||||
| 		// Attach buffers to waveIn API. | ||||
| 		ret, _, _ := waveInPrepareHeader.Call( | ||||
| 			uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 			uintptr(unsafe.Pointer(&buf.waveHdr)), | ||||
| 			uintptr(unsafe.Sizeof(buf.waveHdr)), | ||||
| 		) | ||||
| 		if err := errWinmm[ret]; err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 	for _, buf := range m.buf { | ||||
| 		// Enqueue buffers. | ||||
| 		ret, _, _ := waveInAddBuffer.Call( | ||||
| 			uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 			uintptr(unsafe.Pointer(&buf.waveHdr)), | ||||
| 			uintptr(unsafe.Sizeof(buf.waveHdr)), | ||||
| 		) | ||||
| 		if err := errWinmm[ret]; err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ret, _, _ = waveInStart.Call( | ||||
| 		uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 	) | ||||
| 	if err := errWinmm[ret]; err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// TODO: detect microphone device disconnection and return EOF | ||||
|  | ||||
| 	reader := audio.ReaderFunc(func() (wave.Audio, error) { | ||||
| 		b, ok := <-m.chBuf | ||||
| 		if !ok { | ||||
| 			return nil, io.EOF | ||||
| 		} | ||||
|  | ||||
| 		select { | ||||
| 		case <-m.closed: | ||||
| 		default: | ||||
| 			// Re-enqueue used buffer. | ||||
| 			ret, _, _ := waveInAddBuffer.Call( | ||||
| 				uintptr(unsafe.Pointer(m.hWaveIn)), | ||||
| 				uintptr(unsafe.Pointer(&b.waveHdr)), | ||||
| 				uintptr(unsafe.Sizeof(b.waveHdr)), | ||||
| 			) | ||||
| 			if err := errWinmm[ret]; err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		a := wave.NewInt16Interleaved( | ||||
| 			wave.ChunkInfo{ | ||||
| 				Channels: p.ChannelCount, | ||||
| 				Len:      (int(b.waveHdr.dwBytesRecorded) / 2) / p.ChannelCount, | ||||
| 			}, | ||||
| 		) | ||||
|  | ||||
| 		j := 0 | ||||
| 		for i := 0; i < a.Size.Len; i++ { | ||||
| 			for ch := 0; ch < a.Size.Channels; ch++ { | ||||
| 				a.SetInt16(i, ch, wave.Int16Sample(b.data[j])) | ||||
| 				j++ | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return a, nil | ||||
| 	}) | ||||
| 	return reader, nil | ||||
| } | ||||
|  | ||||
| func (m *microphone) Properties() []prop.Media { | ||||
| 	// TODO: Get actual properties | ||||
| 	monoProp := prop.Media{ | ||||
| 		Audio: prop.Audio{ | ||||
| 			SampleRate:   48000, | ||||
| 			Latency:      time.Millisecond * 20, | ||||
| 			ChannelCount: 1, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	stereoProp := monoProp | ||||
| 	stereoProp.ChannelCount = 2 | ||||
|  | ||||
| 	return []prop.Media{monoProp, stereoProp} | ||||
| } | ||||
|  | ||||
| // Windows API structures | ||||
|  | ||||
| type waveFormatEx struct { | ||||
| 	wFormatTag      uint16 | ||||
| 	nChannels       uint16 | ||||
| 	nSamplesPerSec  uint32 | ||||
| 	nAvgBytesPerSec uint32 | ||||
| 	nBlockAlign     uint16 | ||||
| 	wBitsPerSample  uint16 | ||||
| 	cbSize          uint16 | ||||
| } | ||||
|  | ||||
| type waveHdr struct { | ||||
| 	lpData          uintptr | ||||
| 	dwBufferLength  uint32 | ||||
| 	dwBytesRecorded uint32 | ||||
| 	dwUser          *uint32 | ||||
| 	dwFlags         uint32 | ||||
| 	dwLoops         uint32 | ||||
| 	lpNext          *waveHdr | ||||
| 	reserved        *uint32 | ||||
| } | ||||
|  | ||||
| // Windows consts | ||||
|  | ||||
| const ( | ||||
| 	MMSYSERR_NOERROR      = 0 | ||||
| 	MMSYSERR_ERROR        = 1 | ||||
| 	MMSYSERR_BADDEVICEID  = 2 | ||||
| 	MMSYSERR_NOTENABLED   = 3 | ||||
| 	MMSYSERR_ALLOCATED    = 4 | ||||
| 	MMSYSERR_INVALHANDLE  = 5 | ||||
| 	MMSYSERR_NODRIVER     = 6 | ||||
| 	MMSYSERR_NOMEM        = 7 | ||||
| 	MMSYSERR_NOTSUPPORTED = 8 | ||||
| 	MMSYSERR_BADERRNUM    = 9 | ||||
| 	MMSYSERR_INVALFLAG    = 10 | ||||
| 	MMSYSERR_INVALPARAM   = 11 | ||||
| 	MMSYSERR_HANDLEBUSY   = 12 | ||||
| 	MMSYSERR_INVALIDALIAS = 13 | ||||
| 	MMSYSERR_BADDB        = 14 | ||||
| 	MMSYSERR_KEYNOTFOUND  = 15 | ||||
| 	MMSYSERR_READERROR    = 16 | ||||
| 	MMSYSERR_WRITEERROR   = 17 | ||||
| 	MMSYSERR_DELETEERROR  = 18 | ||||
| 	MMSYSERR_VALNOTFOUND  = 19 | ||||
| 	MMSYSERR_NODRIVERCB   = 20 | ||||
|  | ||||
| 	WAVERR_BADFORMAT    = 32 | ||||
| 	WAVERR_STILLPLAYING = 33 | ||||
| 	WAVERR_UNPREPARED   = 34 | ||||
| 	WAVERR_SYNC         = 35 | ||||
|  | ||||
| 	WAVE_MAPPER     = 0xFFFF | ||||
| 	WAVE_FORMAT_PCM = 1 | ||||
|  | ||||
| 	CALLBACK_NULL     = 0 | ||||
| 	CALLBACK_WINDOW   = 0x10000 | ||||
| 	CALLBACK_TASK     = 0x20000 | ||||
| 	CALLBACK_FUNCTION = 0x30000 | ||||
| 	CALLBACK_THREAD   = CALLBACK_TASK | ||||
| 	CALLBACK_EVENT    = 0x50000 | ||||
|  | ||||
| 	MM_WIM_OPEN  = 0x3BE | ||||
| 	MM_WIM_CLOSE = 0x3BF | ||||
| 	MM_WIM_DATA  = 0x3C0 | ||||
| ) | ||||
|  | ||||
| var errWinmm = map[uintptr]error{ | ||||
| 	MMSYSERR_NOERROR:      nil, | ||||
| 	MMSYSERR_ERROR:        errors.New("error"), | ||||
| 	MMSYSERR_BADDEVICEID:  errors.New("bad device id"), | ||||
| 	MMSYSERR_NOTENABLED:   errors.New("not enabled"), | ||||
| 	MMSYSERR_ALLOCATED:    errors.New("already allocated"), | ||||
| 	MMSYSERR_INVALHANDLE:  errors.New("invalid handler"), | ||||
| 	MMSYSERR_NODRIVER:     errors.New("no driver"), | ||||
| 	MMSYSERR_NOMEM:        errors.New("no memory"), | ||||
| 	MMSYSERR_NOTSUPPORTED: errors.New("not supported"), | ||||
| 	MMSYSERR_BADERRNUM:    errors.New("band error number"), | ||||
| 	MMSYSERR_INVALFLAG:    errors.New("invalid flag"), | ||||
| 	MMSYSERR_INVALPARAM:   errors.New("invalid param"), | ||||
| 	MMSYSERR_HANDLEBUSY:   errors.New("handle busy"), | ||||
| 	MMSYSERR_INVALIDALIAS: errors.New("invalid alias"), | ||||
| 	MMSYSERR_BADDB:        errors.New("bad db"), | ||||
| 	MMSYSERR_KEYNOTFOUND:  errors.New("key not found"), | ||||
| 	MMSYSERR_READERROR:    errors.New("read error"), | ||||
| 	MMSYSERR_WRITEERROR:   errors.New("write error"), | ||||
| 	MMSYSERR_DELETEERROR:  errors.New("delete error"), | ||||
| 	MMSYSERR_VALNOTFOUND:  errors.New("value not found"), | ||||
| 	MMSYSERR_NODRIVERCB:   errors.New("no driver cb"), | ||||
| 	WAVERR_BADFORMAT:      errors.New("bad format"), | ||||
| 	WAVERR_STILLPLAYING:   errors.New("still playing"), | ||||
| 	WAVERR_UNPREPARED:     errors.New("unprepared"), | ||||
| 	WAVERR_SYNC:           errors.New("sync"), | ||||
| } | ||||
| @@ -68,9 +68,9 @@ func (s *screen) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 	var dst image.RGBA | ||||
| 	reader := s.reader | ||||
|  | ||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | ||||
| 	r := video.ReaderFunc(func() (image.Image, error) { | ||||
| 		<-s.tick.C | ||||
| 		return reader.Read().ToRGBA(&dst), func() {}, nil | ||||
| 		return reader.Read().ToRGBA(&dst), nil | ||||
| 	}) | ||||
| 	return r, nil | ||||
| } | ||||
|   | ||||
| @@ -103,10 +103,10 @@ func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 	d.tick = tick | ||||
| 	closed := d.closed | ||||
|  | ||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | ||||
| 	r := video.ReaderFunc(func() (image.Image, error) { | ||||
| 		select { | ||||
| 		case <-closed: | ||||
| 			return nil, func() {}, io.EOF | ||||
| 			return nil, io.EOF | ||||
| 		default: | ||||
| 		} | ||||
|  | ||||
| @@ -130,7 +130,7 @@ func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) { | ||||
| 			CStride:        p.Width / 2, | ||||
| 			SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||
| 			Rect:           image.Rect(0, 0, p.Width, p.Height), | ||||
| 		}, func() {}, nil | ||||
| 		}, nil | ||||
| 	}) | ||||
|  | ||||
| 	return r, nil | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import ( | ||||
| 	"image/jpeg" | ||||
| ) | ||||
|  | ||||
| func decodeMJPEG(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 	img, err := jpeg.Decode(bytes.NewReader(frame)) | ||||
| 	return img, func() {}, err | ||||
| func decodeMJPEG(frame []byte, width, height int) (image.Image, error) { | ||||
| 	return jpeg.Decode(bytes.NewReader(frame)) | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,8 @@ package frame | ||||
| type Format string | ||||
|  | ||||
| const ( | ||||
| 	// YUV Formats | ||||
|  | ||||
| 	// FormatI420 https://www.fourcc.org/pixel-format/yuv-i420/ | ||||
| 	FormatI420 Format = "I420" | ||||
| 	// FormatI444 is a YUV format without sub-sampling | ||||
| @@ -14,11 +16,18 @@ const ( | ||||
| 	// FormatUYVY https://www.fourcc.org/pixel-format/yuv-uyvy/ | ||||
| 	FormatUYVY = "UYVY" | ||||
|  | ||||
| 	// RGB Formats | ||||
|  | ||||
| 	// FormatRGBA https://www.fourcc.org/pixel-format/rgb-rgba/ | ||||
| 	FormatRGBA Format = "RGBA" | ||||
|  | ||||
| 	// Compressed Formats | ||||
|  | ||||
| 	// FormatMJPEG https://www.fourcc.org/mjpg/ | ||||
| 	FormatMJPEG = "MJPEG" | ||||
| ) | ||||
|  | ||||
| // YUV aliases | ||||
|  | ||||
| // FormatYUYV is an alias of FormatYUY2 | ||||
| const FormatYUYV = FormatYUY2 | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| func NewDecoder(f Format) (Decoder, error) { | ||||
| 	var decoder decoderFunc | ||||
| 	var decoder DecoderFunc | ||||
|  | ||||
| 	switch f { | ||||
| 	case FormatI420: | ||||
|   | ||||
| @@ -3,12 +3,12 @@ package frame | ||||
| import "image" | ||||
|  | ||||
| type Decoder interface { | ||||
| 	Decode(frame []byte, width, height int) (image.Image, func(), error) | ||||
| 	Decode(frame []byte, width, height int) (image.Image, error) | ||||
| } | ||||
|  | ||||
| // DecoderFunc is a proxy type for Decoder | ||||
| type decoderFunc func(frame []byte, width, height int) (image.Image, func(), error) | ||||
| type DecoderFunc func(frame []byte, width, height int) (image.Image, error) | ||||
|  | ||||
| func (f decoderFunc) Decode(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func (f DecoderFunc) Decode(frame []byte, width, height int) (image.Image, error) { | ||||
| 	return f(frame, width, height) | ||||
| } | ||||
|   | ||||
| @@ -5,13 +5,13 @@ import ( | ||||
| 	"image" | ||||
| ) | ||||
|  | ||||
| func decodeI420(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func decodeI420(frame []byte, width, height int) (image.Image, error) { | ||||
| 	yi := width * height | ||||
| 	cbi := yi + width*height/4 | ||||
| 	cri := cbi + width*height/4 | ||||
|  | ||||
| 	if cri > len(frame) { | ||||
| 		return nil, func() {}, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), cri) | ||||
| 		return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), cri) | ||||
| 	} | ||||
|  | ||||
| 	return &image.YCbCr{ | ||||
| @@ -22,15 +22,15 @@ func decodeI420(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 		CStride:        width / 2, | ||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio420, | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	}, func() {}, nil | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func decodeNV21(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func decodeNV21(frame []byte, width, height int) (image.Image, error) { | ||||
| 	yi := width * height | ||||
| 	ci := yi + width*height/2 | ||||
|  | ||||
| 	if ci > len(frame) { | ||||
| 		return nil, func() {}, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), ci) | ||||
| 		return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), ci) | ||||
| 	} | ||||
|  | ||||
| 	var cb, cr []byte | ||||
| @@ -47,5 +47,5 @@ func decodeNV21(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 		CStride:        width / 2, | ||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio420, | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	}, func() {}, nil | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
| @@ -12,13 +12,13 @@ import ( | ||||
| // void decodeUYVYCGO(uint8_t* y, uint8_t* cb, uint8_t* cr, uint8_t* uyvy, int width, int height); | ||||
| import "C" | ||||
|  | ||||
| func decodeYUY2(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func decodeYUY2(frame []byte, width, height int) (image.Image, error) { | ||||
| 	yi := width * height | ||||
| 	ci := yi / 2 | ||||
| 	fi := yi + 2*ci | ||||
|  | ||||
| 	if len(frame) != fi { | ||||
| 		return nil, func() {}, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 		return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 	} | ||||
|  | ||||
| 	y := make([]byte, yi) | ||||
| @@ -41,16 +41,16 @@ func decodeYUY2(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 		CStride:        width / 2, | ||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	}, func() {}, nil | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func decodeUYVY(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func decodeUYVY(frame []byte, width, height int) (image.Image, error) { | ||||
| 	yi := width * height | ||||
| 	ci := yi / 2 | ||||
| 	fi := yi + 2*ci | ||||
|  | ||||
| 	if len(frame) != fi { | ||||
| 		return nil, func() {}, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 		return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 	} | ||||
|  | ||||
| 	y := make([]byte, yi) | ||||
| @@ -73,5 +73,5 @@ func decodeUYVY(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 		CStride:        width / 2, | ||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	}, func() {}, nil | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
| @@ -7,13 +7,13 @@ import ( | ||||
| 	"image" | ||||
| ) | ||||
|  | ||||
| func decodeYUY2(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func decodeYUY2(frame []byte, width, height int) (image.Image, error) { | ||||
| 	yi := width * height | ||||
| 	ci := yi / 2 | ||||
| 	fi := yi + 2*ci | ||||
|  | ||||
| 	if len(frame) != fi { | ||||
| 		return nil, func() {}, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 		return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 	} | ||||
|  | ||||
| 	y := make([]byte, yi) | ||||
| @@ -39,16 +39,16 @@ func decodeYUY2(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 		CStride:        width / 2, | ||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	}, func() {}, nil | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func decodeUYVY(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| func decodeUYVY(frame []byte, width, height int) (image.Image, error) { | ||||
| 	yi := width * height | ||||
| 	ci := yi / 2 | ||||
| 	fi := yi + 2*ci | ||||
|  | ||||
| 	if len(frame) != fi { | ||||
| 		return nil, func() {}, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 		return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), fi) | ||||
| 	} | ||||
|  | ||||
| 	y := make([]byte, yi) | ||||
| @@ -74,5 +74,5 @@ func decodeUYVY(frame []byte, width, height int) (image.Image, func(), error) { | ||||
| 		CStride:        width / 2, | ||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	}, func() {}, nil | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
| @@ -27,7 +27,7 @@ func TestDecodeYUY2(t *testing.T) { | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := decodeYUY2(input, width, height) | ||||
| 	img, err := decodeYUY2(input, width, height) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| @@ -56,7 +56,7 @@ func TestDecodeUYVY(t *testing.T) { | ||||
| 		Rect:           image.Rect(0, 0, width, height), | ||||
| 	} | ||||
|  | ||||
| 	img, _, err := decodeUYVY(input, width, height) | ||||
| 	img, err := decodeUYVY(input, width, height) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| @@ -77,7 +77,7 @@ func BenchmarkDecodeYUY2(b *testing.B) { | ||||
| 		b.Run(fmt.Sprintf("%dx%d", sz.width, sz.height), func(b *testing.B) { | ||||
| 			input := make([]byte, sz.width*sz.height*2) | ||||
| 			for i := 0; i < b.N; i++ { | ||||
| 				_, _, err := decodeYUY2(input, sz.width, sz.height) | ||||
| 				_, err := decodeYUY2(input, sz.width, sz.height) | ||||
| 				if err != nil { | ||||
| 					b.Fatal(err) | ||||
| 				} | ||||
|   | ||||
| @@ -5,14 +5,13 @@ import ( | ||||
| ) | ||||
|  | ||||
| type Reader interface { | ||||
| 	Read() (chunk wave.Audio, release func(), err error) | ||||
| 	Read() (wave.Audio, error) | ||||
| } | ||||
|  | ||||
| type ReaderFunc func() (chunk wave.Audio, release func(), err error) | ||||
| type ReaderFunc func() (wave.Audio, error) | ||||
|  | ||||
| func (rf ReaderFunc) Read() (chunk wave.Audio, release func(), err error) { | ||||
| 	chunk, release, err = rf() | ||||
| 	return | ||||
| func (rf ReaderFunc) Read() (wave.Audio, error) { | ||||
| 	return rf() | ||||
| } | ||||
|  | ||||
| // TransformFunc produces a new Reader that will produces a transformed audio | ||||
|   | ||||
| @@ -1,76 +0,0 @@ | ||||
| package audio | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/io" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| var errEmptySource = errors.New("Source can't be nil") | ||||
|  | ||||
| // Broadcaster is a specialized video broadcaster. | ||||
| type Broadcaster struct { | ||||
| 	ioBroadcaster *io.Broadcaster | ||||
| } | ||||
|  | ||||
| type BroadcasterConfig struct { | ||||
| 	Core *io.BroadcasterConfig | ||||
| } | ||||
|  | ||||
| // NewBroadcaster creates a new broadcaster. Source is expected to drop chunks | ||||
| // when any of the readers is slower than the source. | ||||
| func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster { | ||||
| 	var coreConfig *io.BroadcasterConfig | ||||
|  | ||||
| 	if config != nil { | ||||
| 		coreConfig = config.Core | ||||
| 	} | ||||
|  | ||||
| 	broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, func(), error) { | ||||
| 		return source.Read() | ||||
| 	}), coreConfig) | ||||
|  | ||||
| 	return &Broadcaster{broadcaster} | ||||
| } | ||||
|  | ||||
| // NewReader creates a new reader. Each reader will retrieve the same data from the source. | ||||
| // copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring | ||||
| // buffer, this means that slow readers might miss some data if they're really late and the data is no longer | ||||
| // in the ring buffer. | ||||
| func (broadcaster *Broadcaster) NewReader(copyChunk bool) Reader { | ||||
| 	copyFn := func(src interface{}) interface{} { return src } | ||||
|  | ||||
| 	if copyChunk { | ||||
| 		buffer := wave.NewBuffer() | ||||
| 		copyFn = func(src interface{}) interface{} { | ||||
| 			realSrc, _ := src.(wave.Audio) | ||||
| 			buffer.StoreCopy(realSrc) | ||||
| 			return buffer.Load() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	reader := broadcaster.ioBroadcaster.NewReader(copyFn) | ||||
| 	return ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 		data, _, err := reader.Read() | ||||
| 		chunk, _ := data.(wave.Audio) | ||||
| 		return chunk, func() {}, err | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // ReplaceSource replaces the underlying source. This operation is thread safe. | ||||
| func (broadcaster *Broadcaster) ReplaceSource(source Reader) error { | ||||
| 	return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, func(), error) { | ||||
| 		return source.Read() | ||||
| 	})) | ||||
| } | ||||
|  | ||||
| // Source retrieves the underlying source. This operation is thread safe. | ||||
| func (broadcaster *Broadcaster) Source() Reader { | ||||
| 	source := broadcaster.ioBroadcaster.Source() | ||||
| 	return ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 		data, _, err := source.Read() | ||||
| 		img, _ := data.(wave.Audio) | ||||
| 		return img, func() {}, err | ||||
| 	}) | ||||
| } | ||||
| @@ -1,54 +0,0 @@ | ||||
| package audio | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| func TestBroadcast(t *testing.T) { | ||||
| 	chunk := wave.NewFloat32Interleaved(wave.ChunkInfo{ | ||||
| 		Len:          8, | ||||
| 		Channels:     2, | ||||
| 		SamplingRate: 48000, | ||||
| 	}) | ||||
|  | ||||
| 	source := ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 		return chunk, func() {}, nil | ||||
| 	}) | ||||
|  | ||||
| 	broadcaster := NewBroadcaster(source, nil) | ||||
| 	readerWithoutCopy1 := broadcaster.NewReader(false) | ||||
| 	readerWithoutCopy2 := broadcaster.NewReader(false) | ||||
| 	actualWithoutCopy1, _, err := readerWithoutCopy1.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	actualWithoutCopy2, _, err := readerWithoutCopy2.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if &actualWithoutCopy1.(*wave.Float32Interleaved).Data[0] != &actualWithoutCopy2.(*wave.Float32Interleaved).Data[0] { | ||||
| 		t.Fatal("Expected underlying buffer for frame with copy to be the same from broadcaster's buffer") | ||||
| 	} | ||||
|  | ||||
| 	if !reflect.DeepEqual(chunk, actualWithoutCopy1) { | ||||
| 		t.Fatal("Expected actual frame without copy to be the same with the original") | ||||
| 	} | ||||
|  | ||||
| 	readerWithCopy := broadcaster.NewReader(true) | ||||
| 	actualWithCopy, _, err := readerWithCopy.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if &actualWithCopy.(*wave.Float32Interleaved).Data[0] == &actualWithoutCopy1.(*wave.Float32Interleaved).Data[0] { | ||||
| 		t.Fatal("Expected underlying buffer for frame with copy to be different from broadcaster's buffer") | ||||
| 	} | ||||
|  | ||||
| 	if !reflect.DeepEqual(chunk, actualWithCopy) { | ||||
| 		t.Fatal("Expected actual frame without copy to be the same with the original") | ||||
| 	} | ||||
| } | ||||
| @@ -13,15 +13,15 @@ func NewBuffer(nSamples int) TransformFunc { | ||||
| 	var inBuff wave.Audio | ||||
|  | ||||
| 	return func(r Reader) Reader { | ||||
| 		return ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 		return ReaderFunc(func() (wave.Audio, error) { | ||||
| 			for { | ||||
| 				if inBuff != nil && inBuff.ChunkInfo().Len >= nSamples { | ||||
| 					break | ||||
| 				} | ||||
|  | ||||
| 				buff, _, err := r.Read() | ||||
| 				buff, err := r.Read() | ||||
| 				if err != nil { | ||||
| 					return nil, func() {}, err | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				switch b := buff.(type) { | ||||
| 				case *wave.Float32Interleaved: | ||||
| @@ -59,7 +59,7 @@ func NewBuffer(nSamples int) TransformFunc { | ||||
| 					ib.Size.Len += b.Size.Len | ||||
|  | ||||
| 				default: | ||||
| 					return nil, func() {}, errUnsupported | ||||
| 					return nil, errUnsupported | ||||
| 				} | ||||
| 			} | ||||
| 			switch ib := inBuff.(type) { | ||||
| @@ -71,7 +71,7 @@ func NewBuffer(nSamples int) TransformFunc { | ||||
| 				copy(ibCopy.Data, ib.Data) | ||||
| 				ib.Data = ib.Data[n:] | ||||
| 				ib.Size.Len -= nSamples | ||||
| 				return &ibCopy, func() {}, nil | ||||
| 				return &ibCopy, nil | ||||
|  | ||||
| 			case *wave.Float32Interleaved: | ||||
| 				ibCopy := *ib | ||||
| @@ -81,9 +81,9 @@ func NewBuffer(nSamples int) TransformFunc { | ||||
| 				copy(ibCopy.Data, ib.Data) | ||||
| 				ib.Data = ib.Data[n:] | ||||
| 				ib.Size.Len -= nSamples | ||||
| 				return &ibCopy, func() {}, nil | ||||
| 				return &ibCopy, nil | ||||
| 			} | ||||
| 			return nil, func() {}, errUnsupported | ||||
| 			return nil, errUnsupported | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -49,16 +49,16 @@ func TestBuffer(t *testing.T) { | ||||
| 	trans := NewBuffer(3) | ||||
|  | ||||
| 	var iSent int | ||||
| 	r := trans(ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 	r := trans(ReaderFunc(func() (wave.Audio, error) { | ||||
| 		if iSent < len(input) { | ||||
| 			iSent++ | ||||
| 			return input[iSent-1], func() {}, nil | ||||
| 			return input[iSent-1], nil | ||||
| 		} | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return nil, io.EOF | ||||
| 	})) | ||||
|  | ||||
| 	for i := 0; ; i++ { | ||||
| 		a, _, err := r.Read() | ||||
| 		a, err := r.Read() | ||||
| 		if err != nil { | ||||
| 			if err == io.EOF && i >= len(expected) { | ||||
| 				break | ||||
|   | ||||
| @@ -1,55 +0,0 @@ | ||||
| package audio | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| // DetectChanges will detect chunk and audio property changes. For audio property detection, | ||||
| // since it's time related, interval will be used to determine the sample rate. | ||||
| func DetectChanges(interval time.Duration, onChange func(prop.Media)) TransformFunc { | ||||
| 	return func(r Reader) Reader { | ||||
| 		var currentProp prop.Media | ||||
| 		var chunkCount uint | ||||
| 		return ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 			var dirty bool | ||||
|  | ||||
| 			chunk, _, err := r.Read() | ||||
| 			if err != nil { | ||||
| 				return nil, func() {}, err | ||||
| 			} | ||||
|  | ||||
| 			info := chunk.ChunkInfo() | ||||
| 			if currentProp.ChannelCount != info.Channels { | ||||
| 				currentProp.ChannelCount = info.Channels | ||||
| 				dirty = true | ||||
| 			} | ||||
|  | ||||
| 			if currentProp.SampleRate != info.SamplingRate { | ||||
| 				currentProp.SampleRate = info.SamplingRate | ||||
| 				dirty = true | ||||
| 			} | ||||
|  | ||||
| 			var latency time.Duration | ||||
| 			if currentProp.SampleRate != 0 { | ||||
| 				latency = time.Duration(chunk.ChunkInfo().Len) * time.Second / time.Nanosecond / time.Duration(currentProp.SampleRate) | ||||
| 			} | ||||
| 			if currentProp.Latency != latency { | ||||
| 				currentProp.Latency = latency | ||||
| 				dirty = true | ||||
| 			} | ||||
|  | ||||
| 			// TODO: Also detect sample format changes? | ||||
| 			// TODO: Add audio detect changes. As of now, there's no useful property to track. | ||||
|  | ||||
| 			if dirty { | ||||
| 				onChange(currentProp) | ||||
| 			} | ||||
|  | ||||
| 			chunkCount++ | ||||
| 			return chunk, func() {}, nil | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -1,76 +0,0 @@ | ||||
| package audio | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| ) | ||||
|  | ||||
| func TestDetectChanges(t *testing.T) { | ||||
| 	buildSource := func(p prop.Media) (Reader, func(prop.Media)) { | ||||
| 		return ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 				return wave.NewFloat32Interleaved(wave.ChunkInfo{ | ||||
| 					Len:          960, | ||||
| 					Channels:     p.ChannelCount, | ||||
| 					SamplingRate: p.SampleRate, | ||||
| 				}), func() {}, nil | ||||
| 			}), func(newProp prop.Media) { | ||||
| 				p = newProp | ||||
| 			} | ||||
| 	} | ||||
|  | ||||
| 	t.Run("OnChangeCalledBeforeFirstFrame", func(t *testing.T) { | ||||
| 		var detectBeforeFirstChunk bool | ||||
| 		var expected prop.Media | ||||
| 		var actual prop.Media | ||||
| 		expected.ChannelCount = 2 | ||||
| 		expected.SampleRate = 48000 | ||||
| 		expected.Latency = time.Millisecond * 20 | ||||
| 		src, _ := buildSource(expected) | ||||
| 		src = DetectChanges(time.Second, func(p prop.Media) { | ||||
| 			actual = p | ||||
| 			detectBeforeFirstChunk = true | ||||
| 		})(src) | ||||
|  | ||||
| 		_, _, err := src.Read() | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if !detectBeforeFirstChunk { | ||||
| 			t.Fatal("on change callback should have called before first chunk") | ||||
| 		} | ||||
|  | ||||
| 		if !reflect.DeepEqual(actual, expected) { | ||||
| 			t.Fatalf("Received an unexpected prop\nExpected:\n%v\nActual:\n%v\n", expected, actual) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("DetectChangesOnEveryUpdate", func(t *testing.T) { | ||||
| 		var expected prop.Media | ||||
| 		var actual prop.Media | ||||
| 		expected.ChannelCount = 2 | ||||
| 		expected.SampleRate = 48000 | ||||
| 		expected.Latency = 20 * time.Millisecond | ||||
| 		src, update := buildSource(expected) | ||||
| 		src = DetectChanges(time.Second, func(p prop.Media) { | ||||
| 			actual = p | ||||
| 		})(src) | ||||
|  | ||||
| 		for channelCount := 1; channelCount < 8; channelCount++ { | ||||
| 			expected.ChannelCount = channelCount | ||||
| 			update(expected) | ||||
| 			_, _, err := src.Read() | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			if !reflect.DeepEqual(actual, expected) { | ||||
| 				t.Fatalf("Received an unexpected prop\nExpected:\n%v\nActual:\n%v\n", expected, actual) | ||||
| 			} | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| @@ -8,14 +8,14 @@ import ( | ||||
| // NewChannelMixer creates audio transform to mix audio channels. | ||||
| func NewChannelMixer(channels int, mixer mixer.ChannelMixer) TransformFunc { | ||||
| 	return func(r Reader) Reader { | ||||
| 		return ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 			buff, _, err := r.Read() | ||||
| 		return ReaderFunc(func() (wave.Audio, error) { | ||||
| 			buff, err := r.Read() | ||||
| 			if err != nil { | ||||
| 				return nil, func() {}, err | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			ci := buff.ChunkInfo() | ||||
| 			if ci.Channels == channels { | ||||
| 				return buff, func() {}, nil | ||||
| 				return buff, nil | ||||
| 			} | ||||
|  | ||||
| 			ci.Channels = channels | ||||
| @@ -32,9 +32,9 @@ func NewChannelMixer(channels int, mixer mixer.ChannelMixer) TransformFunc { | ||||
| 				mixed = wave.NewFloat32NonInterleaved(ci) | ||||
| 			} | ||||
| 			if err := mixer.Mix(mixed, buff); err != nil { | ||||
| 				return nil, func() {}, err | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			return mixed, func() {}, nil | ||||
| 			return mixed, nil | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -34,16 +34,16 @@ func TestMixer(t *testing.T) { | ||||
| 	trans := NewChannelMixer(1, &mixer.MonoMixer{}) | ||||
|  | ||||
| 	var iSent int | ||||
| 	r := trans(ReaderFunc(func() (wave.Audio, func(), error) { | ||||
| 	r := trans(ReaderFunc(func() (wave.Audio, error) { | ||||
| 		if iSent < len(input) { | ||||
| 			iSent++ | ||||
| 			return input[iSent-1], func() {}, nil | ||||
| 			return input[iSent-1], nil | ||||
| 		} | ||||
| 		return nil, func() {}, io.EOF | ||||
| 		return nil, io.EOF | ||||
| 	})) | ||||
|  | ||||
| 	for i := 0; ; i++ { | ||||
| 		a, _, err := r.Read() | ||||
| 		a, err := r.Read() | ||||
| 		if err != nil { | ||||
| 			if err == io.EOF && i >= len(expected) { | ||||
| 				break | ||||
|   | ||||
| @@ -1,162 +0,0 @@ | ||||
| package io | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	maskReading                = 1 << 63 | ||||
| 	defaultBroadcasterRingSize = 32 | ||||
| 	// TODO: If the data source has fps greater than 30, they'll see some | ||||
| 	// 			 fps fluctuation. But, 30 fps should be enough for general cases. | ||||
| 	defaultBroadcasterRingPollDuration = time.Millisecond * 33 | ||||
| ) | ||||
|  | ||||
| var errEmptySource = fmt.Errorf("Source can't be nil") | ||||
|  | ||||
| type broadcasterData struct { | ||||
| 	data  interface{} | ||||
| 	count uint32 | ||||
| 	err   error | ||||
| } | ||||
|  | ||||
| type broadcasterRing struct { | ||||
| 	// reading (1 bit) + reserved (31 bits) + data count (32 bits) | ||||
| 	// IMPORTANT: state has to be the first element in struct, otherwise LoadUint64 will panic in 32 bits systems | ||||
| 	//            due to unallignment | ||||
| 	state        uint64 | ||||
| 	buffer       []atomic.Value | ||||
| 	pollDuration time.Duration | ||||
| } | ||||
|  | ||||
| func newBroadcasterRing(size uint, pollDuration time.Duration) *broadcasterRing { | ||||
| 	return &broadcasterRing{buffer: make([]atomic.Value, size), pollDuration: pollDuration} | ||||
| } | ||||
|  | ||||
| func (ring *broadcasterRing) index(count uint32) int { | ||||
| 	return int(count) % len(ring.buffer) | ||||
| } | ||||
|  | ||||
| func (ring *broadcasterRing) acquire(count uint32) func(*broadcasterData) { | ||||
| 	// Reader has reached the latest data, should read from the source. | ||||
| 	// Only allow 1 reader to read from the source. When there are more than 1 readers, | ||||
| 	// the other readers will need to share the same data that the first reader gets from | ||||
| 	// the source. | ||||
| 	state := uint64(count) | ||||
| 	if atomic.CompareAndSwapUint64(&ring.state, state, state|maskReading) { | ||||
| 		return func(data *broadcasterData) { | ||||
| 			i := ring.index(count) | ||||
| 			ring.buffer[i].Store(data) | ||||
| 			atomic.StoreUint64(&ring.state, uint64(count+1)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (ring *broadcasterRing) get(count uint32) *broadcasterData { | ||||
| 	for { | ||||
| 		reading := uint64(count) | maskReading | ||||
| 		// TODO: since it's lockless, it spends a lot of resources in the scheduling. | ||||
| 		for atomic.LoadUint64(&ring.state) == reading { | ||||
| 			// Yield current goroutine to let other goroutines to run instead | ||||
| 			time.Sleep(ring.pollDuration) | ||||
| 		} | ||||
|  | ||||
| 		i := ring.index(count) | ||||
| 		data := ring.buffer[i].Load().(*broadcasterData) | ||||
| 		if data.count == count { | ||||
| 			return data | ||||
| 		} | ||||
|  | ||||
| 		count++ | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (ring *broadcasterRing) lastCount() uint32 { | ||||
| 	// ring.state always keeps track the next count, so we need to subtract it by 1 to get the | ||||
| 	// last count | ||||
| 	return uint32(atomic.LoadUint64(&ring.state)) - 1 | ||||
| } | ||||
|  | ||||
| // Broadcaster is a generic pull-based broadcaster. Broadcaster is unique in a sense that | ||||
| // readers can come and go at anytime, and readers don't need to close or notify broadcaster. | ||||
| type Broadcaster struct { | ||||
| 	source atomic.Value | ||||
| 	buffer *broadcasterRing | ||||
| } | ||||
|  | ||||
| // BroadcasterConfig is a config to control broadcaster behaviour | ||||
| type BroadcasterConfig struct { | ||||
| 	// BufferSize configures the underlying ring buffer size that's being used | ||||
| 	// to avoid data lost for late readers. The default value is 32. | ||||
| 	BufferSize uint | ||||
| 	// PollDuration configures the sleep duration in waiting for new data to come. | ||||
| 	// The default value is 33 ms. | ||||
| 	PollDuration time.Duration | ||||
| } | ||||
|  | ||||
| // NewBroadcaster creates a new broadcaster. Source is expected to drop frames | ||||
| // when any of the readers is slower than the source. | ||||
| func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster { | ||||
| 	pollDuration := defaultBroadcasterRingPollDuration | ||||
| 	var bufferSize uint = defaultBroadcasterRingSize | ||||
| 	if config != nil { | ||||
| 		if config.PollDuration != 0 { | ||||
| 			pollDuration = config.PollDuration | ||||
| 		} | ||||
|  | ||||
| 		if config.BufferSize != 0 { | ||||
| 			bufferSize = config.BufferSize | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var broadcaster Broadcaster | ||||
| 	broadcaster.buffer = newBroadcasterRing(bufferSize, pollDuration) | ||||
| 	broadcaster.ReplaceSource(source) | ||||
|  | ||||
| 	return &broadcaster | ||||
| } | ||||
|  | ||||
| // NewReader creates a new reader. Each reader will retrieve the same data from the source. | ||||
| // copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring | ||||
| // buffer, this means that slow readers might miss some data if they're really late and the data is no longer | ||||
| // in the ring buffer. | ||||
| func (broadcaster *Broadcaster) NewReader(copyFn func(interface{}) interface{}) Reader { | ||||
| 	currentCount := broadcaster.buffer.lastCount() | ||||
|  | ||||
| 	return ReaderFunc(func() (data interface{}, release func(), err error) { | ||||
| 		currentCount++ | ||||
| 		if push := broadcaster.buffer.acquire(currentCount); push != nil { | ||||
| 			data, _, err = broadcaster.source.Load().(Reader).Read() | ||||
| 			push(&broadcasterData{ | ||||
| 				data:  data, | ||||
| 				err:   err, | ||||
| 				count: currentCount, | ||||
| 			}) | ||||
| 		} else { | ||||
| 			ringData := broadcaster.buffer.get(currentCount) | ||||
| 			data, err, currentCount = ringData.data, ringData.err, ringData.count | ||||
| 		} | ||||
|  | ||||
| 		data = copyFn(data) | ||||
| 		return | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // ReplaceSource replaces the underlying source. This operation is thread safe. | ||||
| func (broadcaster *Broadcaster) ReplaceSource(source Reader) error { | ||||
| 	if source == nil { | ||||
| 		return errEmptySource | ||||
| 	} | ||||
|  | ||||
| 	broadcaster.source.Store(source) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // ReplaceSource retrieves the underlying source. This operation is thread safe. | ||||
| func (broadcaster *Broadcaster) Source() Reader { | ||||
| 	return broadcaster.source.Load().(Reader) | ||||
| } | ||||
| @@ -1,148 +0,0 @@ | ||||
| package io | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"runtime" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func TestBroadcast(t *testing.T) { | ||||
| 	// https://github.com/pion/mediadevices/issues/198 | ||||
| 	if runtime.GOOS == "darwin" { | ||||
| 		t.Skip("Skipping because Darwin CI is not reliable for timing related tests.") | ||||
| 	} | ||||
| 	frames := make([]int, 5*30) // 5 seconds worth of frames | ||||
| 	for i := range frames { | ||||
| 		frames[i] = i | ||||
| 	} | ||||
|  | ||||
| 	routinePauseConds := []struct { | ||||
| 		src          bool | ||||
| 		dst          bool | ||||
| 		expectedFPS  float64 | ||||
| 		expectedDrop float64 | ||||
| 	}{ | ||||
| 		{ | ||||
| 			src:         false, | ||||
| 			dst:         false, | ||||
| 			expectedFPS: 30, | ||||
| 		}, | ||||
| 		{ | ||||
| 			src:          true, | ||||
| 			dst:          false, | ||||
| 			expectedFPS:  20, | ||||
| 			expectedDrop: 10, | ||||
| 		}, | ||||
| 		{ | ||||
| 			src:          false, | ||||
| 			dst:          true, | ||||
| 			expectedFPS:  20, | ||||
| 			expectedDrop: 10, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, pauseCond := range routinePauseConds { | ||||
| 		pauseCond := pauseCond | ||||
| 		t.Run(fmt.Sprintf("SrcPause-%v/DstPause-%v", pauseCond.src, pauseCond.dst), func(t *testing.T) { | ||||
| 			for n := 1; n <= 256; n *= 16 { | ||||
| 				n := n | ||||
|  | ||||
| 				t.Run(fmt.Sprintf("Readers-%d", n), func(t *testing.T) { | ||||
| 					var src Reader | ||||
| 					interval := time.NewTicker(time.Millisecond * 33) // 30 fps | ||||
| 					defer interval.Stop() | ||||
| 					frameCount := 0 | ||||
| 					frameSent := 0 | ||||
| 					lastSend := time.Now() | ||||
| 					src = ReaderFunc(func() (interface{}, func(), error) { | ||||
| 						if pauseCond.src && frameSent == 30 { | ||||
| 							time.Sleep(time.Second) | ||||
| 						} | ||||
| 						<-interval.C | ||||
|  | ||||
| 						now := time.Now() | ||||
| 						if interval := now.Sub(lastSend); interval > time.Millisecond*33*3/2 { | ||||
| 							// Source reader should drop frames to catch up the latest frame. | ||||
| 							drop := int(interval/(time.Millisecond*33)) - 1 | ||||
| 							frameCount += drop | ||||
| 							t.Logf("Skipped %d frames", drop) | ||||
| 						} | ||||
| 						lastSend = now | ||||
| 						frame := frames[frameCount] | ||||
| 						frameCount++ | ||||
| 						frameSent++ | ||||
| 						return frame, func() {}, nil | ||||
| 					}) | ||||
| 					broadcaster := NewBroadcaster(src, nil) | ||||
| 					var done uint32 | ||||
| 					duration := time.Second * 3 | ||||
| 					fpsChan := make(chan []float64) | ||||
|  | ||||
| 					var wg sync.WaitGroup | ||||
| 					wg.Add(n) | ||||
| 					for i := 0; i < n; i++ { | ||||
| 						go func() { | ||||
| 							reader := broadcaster.NewReader(func(src interface{}) interface{} { return src }) | ||||
| 							count := 0 | ||||
| 							lastFrameCount := -1 | ||||
| 							droppedFrames := 0 | ||||
| 							wg.Done() | ||||
| 							wg.Wait() | ||||
| 							for atomic.LoadUint32(&done) == 0 { | ||||
| 								if pauseCond.dst && count == 30 { | ||||
| 									time.Sleep(time.Second) | ||||
| 								} | ||||
| 								frame, _, err := reader.Read() | ||||
| 								if err != nil { | ||||
| 									t.Error(err) | ||||
| 								} | ||||
| 								frameCount := frame.(int) | ||||
| 								droppedFrames += (frameCount - lastFrameCount - 1) | ||||
| 								lastFrameCount = frameCount | ||||
| 								count++ | ||||
| 							} | ||||
|  | ||||
| 							fps := float64(count) / duration.Seconds() | ||||
| 							if fps < pauseCond.expectedFPS-2 || fps > pauseCond.expectedFPS+2 { | ||||
| 								t.Fatal("Unexpected average FPS") | ||||
| 							} | ||||
|  | ||||
| 							droppedFramesPerSecond := float64(droppedFrames) / duration.Seconds() | ||||
| 							if droppedFramesPerSecond < pauseCond.expectedDrop-2 || droppedFramesPerSecond > pauseCond.expectedDrop+2 { | ||||
| 								t.Fatal("Unexpected drop count") | ||||
| 							} | ||||
|  | ||||
| 							fpsChan <- []float64{fps, droppedFramesPerSecond, float64(lastFrameCount)} | ||||
| 						}() | ||||
| 					} | ||||
|  | ||||
| 					time.Sleep(duration) | ||||
| 					atomic.StoreUint32(&done, 1) | ||||
|  | ||||
| 					var fpsAvg float64 | ||||
| 					var droppedFramesPerSecondAvg float64 | ||||
| 					var lastFrameCountAvg float64 | ||||
| 					var count int | ||||
| 					for metric := range fpsChan { | ||||
| 						fps, droppedFramesPerSecond, lastFrameCount := metric[0], metric[1], metric[2] | ||||
| 						fpsAvg += fps | ||||
| 						droppedFramesPerSecondAvg += droppedFramesPerSecond | ||||
| 						lastFrameCountAvg += lastFrameCount | ||||
| 						count++ | ||||
| 						if count == n { | ||||
| 							break | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					t.Log("Average FPS                      :", fpsAvg/float64(n)) | ||||
| 					t.Log("Average dropped frames per second:", droppedFramesPerSecondAvg/float64(n)) | ||||
| 					t.Log("Last frame count (src)           :", frameCount) | ||||
| 					t.Log("Average last frame count (dst)   :", lastFrameCountAvg/float64(n)) | ||||
| 				}) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										11
									
								
								pkg/io/io.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								pkg/io/io.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| package io | ||||
|  | ||||
| // Copy copies data from src to dst. If dst is not big enough, return an | ||||
| // InsufficientBufferError. | ||||
| func Copy(dst, src []byte) (n int, err error) { | ||||
| 	if len(dst) < len(src) { | ||||
| 		return 0, &InsufficientBufferError{len(src)} | ||||
| 	} | ||||
|  | ||||
| 	return copy(dst, src), nil | ||||
| } | ||||
							
								
								
									
										45
									
								
								pkg/io/io_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								pkg/io/io_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| package io | ||||
|  | ||||
| import ( | ||||
| 	"log" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestCopy(t *testing.T) { | ||||
| 	var dst []byte | ||||
| 	src := make([]byte, 4) | ||||
|  | ||||
| 	n, err := Copy(dst, src) | ||||
| 	if err == nil { | ||||
| 		t.Fatal("expected err to be non-nill") | ||||
| 	} | ||||
|  | ||||
| 	if n != 0 { | ||||
| 		t.Fatalf("expected n to be 0, but got %d", n) | ||||
| 	} | ||||
|  | ||||
| 	e, ok := err.(*InsufficientBufferError) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("expected error to be InsufficientBufferError") | ||||
| 	} | ||||
|  | ||||
| 	if e.RequiredSize != len(src) { | ||||
| 		t.Fatalf("expected required size to be %d, but got %d", len(src), e.RequiredSize) | ||||
| 	} | ||||
|  | ||||
| 	dst = make([]byte, 2*e.RequiredSize) | ||||
| 	n, err = Copy(dst, src) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("expected to not get an error after expanding the buffer") | ||||
| 	} | ||||
|  | ||||
| 	if n != len(src) { | ||||
| 		t.Fatalf("expected n to be %d, but got %d", len(src), n) | ||||
| 	} | ||||
|  | ||||
| 	for i := 0; i < len(src); i++ { | ||||
| 		if src[i] != dst[i] { | ||||
| 			log.Fatalf("expected value at %d to be %d, but got %d", i, src[i], dst[i]) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| package io | ||||
|  | ||||
| // Reader is a generic data reader. In the future, interface{} should be replaced by a generic type | ||||
| // to provide strong type. | ||||
| type Reader interface { | ||||
| 	Read() (data interface{}, release func(), err error) | ||||
| } | ||||
|  | ||||
| // ReaderFunc is a proxy type for Reader | ||||
| type ReaderFunc func() (data interface{}, release func(), err error) | ||||
|  | ||||
| func (f ReaderFunc) Read() (data interface{}, release func(), err error) { | ||||
| 	data, release, err = f() | ||||
| 	return | ||||
| } | ||||
| @@ -1,76 +0,0 @@ | ||||
| package video | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"image" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/io" | ||||
| ) | ||||
|  | ||||
| var errEmptySource = fmt.Errorf("Source can't be nil") | ||||
|  | ||||
| // Broadcaster is a specialized video broadcaster. | ||||
| type Broadcaster struct { | ||||
| 	ioBroadcaster *io.Broadcaster | ||||
| } | ||||
|  | ||||
| type BroadcasterConfig struct { | ||||
| 	Core *io.BroadcasterConfig | ||||
| } | ||||
|  | ||||
| // NewBroadcaster creates a new broadcaster. Source is expected to drop frames | ||||
| // when any of the readers is slower than the source. | ||||
| func NewBroadcaster(source Reader, config *BroadcasterConfig) *Broadcaster { | ||||
| 	var coreConfig *io.BroadcasterConfig | ||||
|  | ||||
| 	if config != nil { | ||||
| 		coreConfig = config.Core | ||||
| 	} | ||||
|  | ||||
| 	broadcaster := io.NewBroadcaster(io.ReaderFunc(func() (interface{}, func(), error) { | ||||
| 		return source.Read() | ||||
| 	}), coreConfig) | ||||
|  | ||||
| 	return &Broadcaster{broadcaster} | ||||
| } | ||||
|  | ||||
| // NewReader creates a new reader. Each reader will retrieve the same data from the source. | ||||
| // copyFn is used to copy the data from the source to individual readers. Broadcaster uses a small ring | ||||
| // buffer, this means that slow readers might miss some data if they're really late and the data is no longer | ||||
| // in the ring buffer. | ||||
| func (broadcaster *Broadcaster) NewReader(copyFrame bool) Reader { | ||||
| 	copyFn := func(src interface{}) interface{} { return src } | ||||
|  | ||||
| 	if copyFrame { | ||||
| 		buffer := NewFrameBuffer(0) | ||||
| 		copyFn = func(src interface{}) interface{} { | ||||
| 			realSrc, _ := src.(image.Image) | ||||
| 			buffer.StoreCopy(realSrc) | ||||
| 			return buffer.Load() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	reader := broadcaster.ioBroadcaster.NewReader(copyFn) | ||||
| 	return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		data, _, err := reader.Read() | ||||
| 		img, _ := data.(image.Image) | ||||
| 		return img, func() {}, err | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // ReplaceSource replaces the underlying source. This operation is thread safe. | ||||
| func (broadcaster *Broadcaster) ReplaceSource(source Reader) error { | ||||
| 	return broadcaster.ioBroadcaster.ReplaceSource(io.ReaderFunc(func() (interface{}, func(), error) { | ||||
| 		return source.Read() | ||||
| 	})) | ||||
| } | ||||
|  | ||||
| // Source retrieves the underlying source. This operation is thread safe. | ||||
| func (broadcaster *Broadcaster) Source() Reader { | ||||
| 	source := broadcaster.ioBroadcaster.Source() | ||||
| 	return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		data, _, err := source.Read() | ||||
| 		img, _ := data.(image.Image) | ||||
| 		return img, func() {}, err | ||||
| 	}) | ||||
| } | ||||
| @@ -1,49 +0,0 @@ | ||||
| package video | ||||
|  | ||||
| import ( | ||||
| 	"image" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestBroadcast(t *testing.T) { | ||||
| 	resolution := image.Rect(0, 0, 1920, 1080) | ||||
| 	img := image.NewGray(resolution) | ||||
| 	source := ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		return img, func() {}, nil | ||||
| 	}) | ||||
|  | ||||
| 	broadcaster := NewBroadcaster(source, nil) | ||||
| 	readerWithoutCopy1 := broadcaster.NewReader(false) | ||||
| 	readerWithoutCopy2 := broadcaster.NewReader(false) | ||||
| 	actualWithoutCopy1, _, err := readerWithoutCopy1.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	actualWithoutCopy2, _, err := readerWithoutCopy2.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if &actualWithoutCopy1.(*image.Gray).Pix[0] != &actualWithoutCopy2.(*image.Gray).Pix[0] { | ||||
| 		t.Fatal("Expected underlying buffer for frame with copy to be the same from broadcaster's buffer") | ||||
| 	} | ||||
|  | ||||
| 	if !reflect.DeepEqual(img, actualWithoutCopy1) { | ||||
| 		t.Fatal("Expected actual frame without copy to be the same with the original") | ||||
| 	} | ||||
|  | ||||
| 	readerWithCopy := broadcaster.NewReader(true) | ||||
| 	actualWithCopy, _, err := readerWithCopy.Read() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if &actualWithCopy.(*image.Gray).Pix[0] == &actualWithoutCopy1.(*image.Gray).Pix[0] { | ||||
| 		t.Fatal("Expected underlying buffer for frame with copy to be different from broadcaster's buffer") | ||||
| 	} | ||||
|  | ||||
| 	if !reflect.DeepEqual(img, actualWithCopy) { | ||||
| 		t.Fatal("Expected actual frame without copy to be the same with the original") | ||||
| 	} | ||||
| } | ||||
| @@ -63,10 +63,10 @@ func imageToYCbCr(dst *image.YCbCr, src image.Image) { | ||||
| // ToI420 converts r to a new reader that will output images in I420 format | ||||
| func ToI420(r Reader) Reader { | ||||
| 	var yuvImg image.YCbCr | ||||
| 	return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		img, _, err := r.Read() | ||||
| 	return ReaderFunc(func() (image.Image, error) { | ||||
| 		img, err := r.Read() | ||||
| 		if err != nil { | ||||
| 			return nil, func() {}, err | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		imageToYCbCr(&yuvImg, img) | ||||
| @@ -79,11 +79,11 @@ func ToI420(r Reader) Reader { | ||||
| 			i422ToI420(&yuvImg) | ||||
| 		case image.YCbCrSubsampleRatio420: | ||||
| 		default: | ||||
| 			return nil, func() {}, fmt.Errorf("unsupported pixel format: %s", yuvImg.SubsampleRatio) | ||||
| 			return nil, fmt.Errorf("unsupported pixel format: %s", yuvImg.SubsampleRatio) | ||||
| 		} | ||||
|  | ||||
| 		yuvImg.SubsampleRatio = image.YCbCrSubsampleRatio420 | ||||
| 		return &yuvImg, func() {}, nil | ||||
| 		return &yuvImg, nil | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| @@ -130,13 +130,13 @@ func imageToRGBA(dst *image.RGBA, src image.Image) { | ||||
| // ToRGBA converts r to a new reader that will output images in RGBA format | ||||
| func ToRGBA(r Reader) Reader { | ||||
| 	var dst image.RGBA | ||||
| 	return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		img, _, err := r.Read() | ||||
| 	return ReaderFunc(func() (image.Image, error) { | ||||
| 		img, err := r.Read() | ||||
| 		if err != nil { | ||||
| 			return nil, func() {}, err | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		imageToRGBA(&dst, img) | ||||
| 		return &dst, func() {}, nil | ||||
| 		return &dst, nil | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -144,10 +144,10 @@ func TestToI420(t *testing.T) { | ||||
| 	for name, c := range cases { | ||||
| 		c := c | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			r := ToI420(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 				return c.src, func() {}, nil | ||||
| 			r := ToI420(ReaderFunc(func() (image.Image, error) { | ||||
| 				return c.src, nil | ||||
| 			})) | ||||
| 			out, _, err := r.Read() | ||||
| 			out, err := r.Read() | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Unexpected error: %v", err) | ||||
| 			} | ||||
| @@ -199,10 +199,10 @@ func TestToRGBA(t *testing.T) { | ||||
| 	for name, c := range cases { | ||||
| 		c := c | ||||
| 		t.Run(name, func(t *testing.T) { | ||||
| 			r := ToRGBA(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 				return c.src, func() {}, nil | ||||
| 			r := ToRGBA(ReaderFunc(func() (image.Image, error) { | ||||
| 				return c.src, nil | ||||
| 			})) | ||||
| 			out, _, err := r.Read() | ||||
| 			out, err := r.Read() | ||||
| 			if err != nil { | ||||
| 				t.Fatalf("Unexpected error: %v", err) | ||||
| 			} | ||||
| @@ -225,12 +225,12 @@ func BenchmarkToI420(b *testing.B) { | ||||
| 			for name, img := range cases { | ||||
| 				img := img | ||||
| 				b.Run(name, func(b *testing.B) { | ||||
| 					r := ToI420(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 						return img, func() {}, nil | ||||
| 					r := ToI420(ReaderFunc(func() (image.Image, error) { | ||||
| 						return img, nil | ||||
| 					})) | ||||
|  | ||||
| 					for i := 0; i < b.N; i++ { | ||||
| 						_, _, err := r.Read() | ||||
| 						_, err := r.Read() | ||||
| 						if err != nil { | ||||
| 							b.Fatalf("Unexpected error: %v", err) | ||||
| 						} | ||||
| @@ -253,12 +253,12 @@ func BenchmarkToRGBA(b *testing.B) { | ||||
| 			for name, img := range cases { | ||||
| 				img := img | ||||
| 				b.Run(name, func(b *testing.B) { | ||||
| 					r := ToRGBA(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 						return img, func() {}, nil | ||||
| 					r := ToRGBA(ReaderFunc(func() (image.Image, error) { | ||||
| 						return img, nil | ||||
| 					})) | ||||
|  | ||||
| 					for i := 0; i < b.N; i++ { | ||||
| 						_, _, err := r.Read() | ||||
| 						_, err := r.Read() | ||||
| 						if err != nil { | ||||
| 							b.Fatalf("Unexpected error: %v", err) | ||||
| 						} | ||||
|   | ||||
| @@ -14,12 +14,12 @@ func DetectChanges(interval time.Duration, onChange func(prop.Media)) TransformF | ||||
| 		var currentProp prop.Media | ||||
| 		var lastTaken time.Time | ||||
| 		var frames uint | ||||
| 		return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		return ReaderFunc(func() (image.Image, error) { | ||||
| 			var dirty bool | ||||
|  | ||||
| 			img, _, err := r.Read() | ||||
| 			img, err := r.Read() | ||||
| 			if err != nil { | ||||
| 				return nil, func() {}, err | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			bounds := img.Bounds() | ||||
| @@ -52,7 +52,7 @@ func DetectChanges(interval time.Duration, onChange func(prop.Media)) TransformF | ||||
| 			} | ||||
|  | ||||
| 			frames++ | ||||
| 			return img, func() {}, nil | ||||
| 			return img, nil | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -12,8 +12,8 @@ import ( | ||||
|  | ||||
| func BenchmarkDetectChanges(b *testing.B) { | ||||
| 	var src Reader | ||||
| 	src = ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		return image.NewRGBA(image.Rect(0, 0, 1920, 1080)), func() {}, nil | ||||
| 	src = ReaderFunc(func() (image.Image, error) { | ||||
| 		return image.NewRGBA(image.Rect(0, 0, 1920, 1080)), nil | ||||
| 	}) | ||||
|  | ||||
| 	b.Run("WithoutDetectChanges", func(b *testing.B) { | ||||
| @@ -40,8 +40,8 @@ func BenchmarkDetectChanges(b *testing.B) { | ||||
|  | ||||
| func TestDetectChanges(t *testing.T) { | ||||
| 	buildSource := func(p prop.Media) (Reader, func(prop.Media)) { | ||||
| 		return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 				return image.NewRGBA(image.Rect(0, 0, p.Width, p.Height)), func() {}, nil | ||||
| 		return ReaderFunc(func() (image.Image, error) { | ||||
| 				return image.NewRGBA(image.Rect(0, 0, p.Width, p.Height)), nil | ||||
| 			}), func(newProp prop.Media) { | ||||
| 				p = newProp | ||||
| 			} | ||||
| @@ -86,7 +86,7 @@ func TestDetectChanges(t *testing.T) { | ||||
| 			detectBeforeFirstFrame = true | ||||
| 		})(src) | ||||
|  | ||||
| 		frame, _, err := src.Read() | ||||
| 		frame, err := src.Read() | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| @@ -113,7 +113,7 @@ func TestDetectChanges(t *testing.T) { | ||||
| 				expected.Width = width | ||||
| 				expected.Height = height | ||||
| 				update(expected) | ||||
| 				frame, _, err := src.Read() | ||||
| 				frame, err := src.Read() | ||||
| 				if err != nil { | ||||
| 					t.Fatal(err) | ||||
| 				} | ||||
| @@ -143,7 +143,7 @@ func TestDetectChanges(t *testing.T) { | ||||
| 		})(src) | ||||
|  | ||||
| 		for count < 3 { | ||||
| 			frame, _, err := src.Read() | ||||
| 			frame, err := src.Read() | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
|   | ||||
| @@ -1,214 +0,0 @@ | ||||
| package video | ||||
|  | ||||
| import ( | ||||
| 	"image" | ||||
| ) | ||||
|  | ||||
| // FrameBuffer is a buffer that can store any image format. | ||||
| type FrameBuffer struct { | ||||
| 	buffer []uint8 | ||||
| 	tmp    image.Image | ||||
| } | ||||
|  | ||||
| // NewFrameBuffer creates a new FrameBuffer instance and initialize internal buffer | ||||
| // with initialSize | ||||
| func NewFrameBuffer(initialSize int) *FrameBuffer { | ||||
| 	return &FrameBuffer{ | ||||
| 		buffer: make([]uint8, initialSize), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (buff *FrameBuffer) storeInOrder(srcs ...[]uint8) { | ||||
| 	var neededSize int | ||||
|  | ||||
| 	for _, src := range srcs { | ||||
| 		neededSize += len(src) | ||||
| 	} | ||||
|  | ||||
| 	if len(buff.buffer) < neededSize { | ||||
| 		if cap(buff.buffer) >= neededSize { | ||||
| 			buff.buffer = buff.buffer[:neededSize] | ||||
| 		} else { | ||||
| 			buff.buffer = make([]uint8, neededSize) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var currentLen int | ||||
| 	for _, src := range srcs { | ||||
| 		copy(buff.buffer[currentLen:], src) | ||||
| 		currentLen += len(src) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Load loads the current owned image | ||||
| func (buff *FrameBuffer) Load() image.Image { | ||||
| 	return buff.tmp | ||||
| } | ||||
|  | ||||
| // StoreCopy makes a copy of src and store its copy. StoreCopy will reuse as much memory as it can | ||||
| // from the previous copies. For example, if StoreCopy is given an image that has the same resolution | ||||
| // and format from the previous call, StoreCopy will not allocate extra memory and only copy the content | ||||
| // from src to the previous buffer. | ||||
| func (buff *FrameBuffer) StoreCopy(src image.Image) { | ||||
| 	switch src := src.(type) { | ||||
| 	case *image.Alpha: | ||||
| 		clone, ok := buff.tmp.(*image.Alpha) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.Alpha16: | ||||
| 		clone, ok := buff.tmp.(*image.Alpha16) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.CMYK: | ||||
| 		clone, ok := buff.tmp.(*image.CMYK) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.Gray: | ||||
| 		clone, ok := buff.tmp.(*image.Gray) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.Gray16: | ||||
| 		clone, ok := buff.tmp.(*image.Gray16) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.NRGBA: | ||||
| 		clone, ok := buff.tmp.(*image.NRGBA) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.NRGBA64: | ||||
| 		clone, ok := buff.tmp.(*image.NRGBA64) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.RGBA: | ||||
| 		clone, ok := buff.tmp.(*image.RGBA) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.RGBA64: | ||||
| 		clone, ok := buff.tmp.(*image.RGBA64) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		buff.storeInOrder(src.Pix) | ||||
| 		clone.Pix = buff.buffer[:len(src.Pix)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.NYCbCrA: | ||||
| 		clone, ok := buff.tmp.(*image.NYCbCrA) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		var currentLen int | ||||
| 		buff.storeInOrder(src.Y, src.Cb, src.Cr, src.A) | ||||
| 		clone.Y = buff.buffer[currentLen : currentLen+len(src.Y) : currentLen+len(src.Y)] | ||||
| 		currentLen += len(src.Y) | ||||
| 		clone.Cb = buff.buffer[currentLen : currentLen+len(src.Cb) : currentLen+len(src.Cb)] | ||||
| 		currentLen += len(src.Cb) | ||||
| 		clone.Cr = buff.buffer[currentLen : currentLen+len(src.Cr) : currentLen+len(src.Cr)] | ||||
| 		currentLen += len(src.Cr) | ||||
| 		clone.A = buff.buffer[currentLen : currentLen+len(src.A) : currentLen+len(src.A)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	case *image.YCbCr: | ||||
| 		clone, ok := buff.tmp.(*image.YCbCr) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		var currentLen int | ||||
| 		buff.storeInOrder(src.Y, src.Cb, src.Cr) | ||||
| 		clone.Y = buff.buffer[currentLen : currentLen+len(src.Y) : currentLen+len(src.Y)] | ||||
| 		currentLen += len(src.Y) | ||||
| 		clone.Cb = buff.buffer[currentLen : currentLen+len(src.Cb) : currentLen+len(src.Cb)] | ||||
| 		currentLen += len(src.Cb) | ||||
| 		clone.Cr = buff.buffer[currentLen : currentLen+len(src.Cr) : currentLen+len(src.Cr)] | ||||
|  | ||||
| 		buff.tmp = clone | ||||
| 	default: | ||||
| 		var converted image.RGBA | ||||
| 		imageToRGBA(&converted, src) | ||||
| 		buff.StoreCopy(&converted) | ||||
| 	} | ||||
| } | ||||
| @@ -1,195 +0,0 @@ | ||||
| package video | ||||
|  | ||||
| import ( | ||||
| 	"image" | ||||
| 	"math/rand" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func randomize(arr []uint8) { | ||||
| 	for i := range arr { | ||||
| 		arr[i] = uint8(rand.Uint32()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkFrameBufferCopyOptimized(b *testing.B) { | ||||
| 	frameBuffer := NewFrameBuffer(0) | ||||
| 	resolution := image.Rect(0, 0, 1920, 1080) | ||||
| 	src := image.NewYCbCr(resolution, image.YCbCrSubsampleRatio420) | ||||
|  | ||||
| 	for i := 0; i < b.N; i++ { | ||||
| 		frameBuffer.StoreCopy(src) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkFrameBufferCopyNaive(b *testing.B) { | ||||
| 	resolution := image.Rect(0, 0, 1920, 1080) | ||||
| 	src := image.NewYCbCr(resolution, image.YCbCrSubsampleRatio420) | ||||
| 	var dst image.Image | ||||
|  | ||||
| 	for i := 0; i < b.N; i++ { | ||||
| 		clone := *src | ||||
| 		clone.Cb = make([]uint8, len(src.Cb)) | ||||
| 		clone.Cr = make([]uint8, len(src.Cr)) | ||||
| 		clone.Y = make([]uint8, len(src.Y)) | ||||
|  | ||||
| 		copy(clone.Cb, src.Cb) | ||||
| 		copy(clone.Cr, src.Cr) | ||||
| 		copy(clone.Y, src.Y) | ||||
| 		dst = &clone | ||||
| 		_ = dst | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFrameBufferStoreCopyAndLoad(t *testing.T) { | ||||
| 	resolution := image.Rect(0, 0, 16, 8) | ||||
| 	rgbaLike := image.NewRGBA64(resolution) | ||||
| 	randomize(rgbaLike.Pix) | ||||
| 	testCases := map[string]struct { | ||||
| 		New    func() image.Image | ||||
| 		Update func(image.Image) | ||||
| 	}{ | ||||
| 		"Alpha": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.Alpha)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.Alpha) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"Alpha16": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.Alpha16)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.Alpha16) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"CMYK": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.CMYK)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.CMYK) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"Gray": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.Gray)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.Gray) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"Gray16": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.Gray16)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.Gray16) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"NRGBA": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.NRGBA)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.NRGBA) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"NRGBA64": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.NRGBA64)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.NRGBA64) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"RGBA": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.RGBA)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.RGBA) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"RGBA64": { | ||||
| 			New: func() image.Image { | ||||
| 				return (*image.RGBA64)(rgbaLike) | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.RGBA64) | ||||
| 				randomize(img.Pix) | ||||
| 			}, | ||||
| 		}, | ||||
| 		"NYCbCrA": { | ||||
| 			New: func() image.Image { | ||||
| 				img := image.NewNYCbCrA(resolution, image.YCbCrSubsampleRatio420) | ||||
| 				randomize(img.Y) | ||||
| 				randomize(img.Cb) | ||||
| 				randomize(img.Cr) | ||||
| 				randomize(img.A) | ||||
| 				img.CStride = 10 | ||||
| 				img.YStride = 5 | ||||
| 				return img | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.NYCbCrA) | ||||
| 				randomize(img.Y) | ||||
| 				randomize(img.Cb) | ||||
| 				randomize(img.Cr) | ||||
| 				randomize(img.A) | ||||
| 				img.CStride = 3 | ||||
| 				img.YStride = 2 | ||||
| 			}, | ||||
| 		}, | ||||
| 		"YCbCr": { | ||||
| 			New: func() image.Image { | ||||
| 				img := image.NewYCbCr(resolution, image.YCbCrSubsampleRatio420) | ||||
| 				randomize(img.Y) | ||||
| 				randomize(img.Cb) | ||||
| 				randomize(img.Cr) | ||||
| 				img.CStride = 10 | ||||
| 				img.YStride = 5 | ||||
| 				return img | ||||
| 			}, | ||||
| 			Update: func(src image.Image) { | ||||
| 				img := src.(*image.YCbCr) | ||||
| 				randomize(img.Y) | ||||
| 				randomize(img.Cb) | ||||
| 				randomize(img.Cr) | ||||
| 				img.CStride = 3 | ||||
| 				img.YStride = 2 | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	frameBuffer := NewFrameBuffer(0) | ||||
|  | ||||
| 	for name, testCase := range testCases { | ||||
| 		// Since the test also wants to make sure that Copier can convert from 1 type to another, | ||||
| 		// t.Run is not ideal since it'll run the tests separately | ||||
| 		t.Log("Testing", name) | ||||
|  | ||||
| 		src := testCase.New() | ||||
| 		frameBuffer.StoreCopy(src) | ||||
| 		if !reflect.DeepEqual(frameBuffer.Load(), src) { | ||||
| 			t.Fatal("Expected the copied image to be identical with the source") | ||||
| 		} | ||||
|  | ||||
| 		testCase.Update(src) | ||||
| 		frameBuffer.StoreCopy(src) | ||||
| 		if !reflect.DeepEqual(frameBuffer.Load(), src) { | ||||
| 			t.Fatal("Expected the copied image to be identical with the source after an update in source") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -156,10 +156,10 @@ func Scale(width, height int, scaler Scaler) TransformFunc { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 			img, _, err := r.Read() | ||||
| 		return ReaderFunc(func() (image.Image, error) { | ||||
| 			img, err := r.Read() | ||||
| 			if err != nil { | ||||
| 				return nil, func() {}, err | ||||
| 				return nil, err | ||||
| 			} | ||||
|  | ||||
| 			switch v := img.(type) { | ||||
| @@ -169,7 +169,7 @@ func Scale(width, height int, scaler Scaler) TransformFunc { | ||||
| 				scalerCached.Scale(dst, rect, v, v.Rect, draw.Src, nil) | ||||
|  | ||||
| 				cloned := *dst // clone metadata | ||||
| 				return &cloned, func() {}, nil | ||||
| 				return &cloned, nil | ||||
|  | ||||
| 			case *image.YCbCr: | ||||
| 				ycbcrRealloc(v) | ||||
| @@ -184,10 +184,10 @@ func Scale(width, height int, scaler Scaler) TransformFunc { | ||||
| 				scalerCached.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil) | ||||
|  | ||||
| 				cloned := *(imgScaled.(*image.YCbCr)) // clone metadata | ||||
| 				return &cloned, func() {}, nil | ||||
| 				return &cloned, nil | ||||
|  | ||||
| 			default: | ||||
| 				return nil, func() {}, errUnsupportedImageType | ||||
| 				return nil, errUnsupportedImageType | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
|   | ||||
| @@ -215,11 +215,11 @@ func TestScale(t *testing.T) { | ||||
| 				c := c | ||||
| 				t.Run(name, func(t *testing.T) { | ||||
| 					trans := Scale(c.width, c.height, algo) | ||||
| 					r := trans(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 						return c.src, func() {}, nil | ||||
| 					r := trans(ReaderFunc(func() (image.Image, error) { | ||||
| 						return c.src, nil | ||||
| 					})) | ||||
| 					for i := 0; i < 4; i++ { | ||||
| 						out, _, err := r.Read() | ||||
| 						out, err := r.Read() | ||||
| 						if err != nil { | ||||
| 							t.Fatalf("Unexpected error: %v", err) | ||||
| 						} | ||||
| @@ -261,12 +261,12 @@ func BenchmarkScale(b *testing.B) { | ||||
| 						img := img | ||||
| 						b.Run(name, func(b *testing.B) { | ||||
| 							trans := Scale(640, 360, algo) | ||||
| 							r := trans(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 								return img, func() {}, nil | ||||
| 							r := trans(ReaderFunc(func() (image.Image, error) { | ||||
| 								return img, nil | ||||
| 							})) | ||||
|  | ||||
| 							for i := 0; i < b.N; i++ { | ||||
| 								_, _, err := r.Read() | ||||
| 								_, err := r.Read() | ||||
| 								if err != nil { | ||||
| 									b.Fatalf("Unexpected error: %v", err) | ||||
| 								} | ||||
|   | ||||
| @@ -10,16 +10,16 @@ import ( | ||||
| func Throttle(rate float32) TransformFunc { | ||||
| 	return func(r Reader) Reader { | ||||
| 		ticker := time.NewTicker(time.Duration(int64(float64(time.Second) / float64(rate)))) | ||||
| 		return ReaderFunc(func() (image.Image, func(), error) { | ||||
| 		return ReaderFunc(func() (image.Image, error) { | ||||
| 			for { | ||||
| 				img, _, err := r.Read() | ||||
| 				img, err := r.Read() | ||||
| 				if err != nil { | ||||
| 					ticker.Stop() | ||||
| 					return nil, func() {}, err | ||||
| 					return nil, err | ||||
| 				} | ||||
| 				select { | ||||
| 				case <-ticker.C: | ||||
| 					return img, func() {}, nil | ||||
| 					return img, nil | ||||
| 				default: | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
| @@ -19,14 +19,14 @@ func TestThrottle(t *testing.T) { | ||||
|  | ||||
| 	var cntPush int | ||||
| 	trans := Throttle(50) | ||||
| 	r := trans(ReaderFunc(func() (image.Image, func(), error) { | ||||
| 	r := trans(ReaderFunc(func() (image.Image, error) { | ||||
| 		<-ticker.C | ||||
| 		cntPush++ | ||||
| 		return img, func() {}, nil | ||||
| 		return img, nil | ||||
| 	})) | ||||
|  | ||||
| 	for i := 0; i < 20; i++ { | ||||
| 		_, _, err := r.Read() | ||||
| 		_, err := r.Read() | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Unexpected error: %v", err) | ||||
| 		} | ||||
|   | ||||
| @@ -5,14 +5,13 @@ import ( | ||||
| ) | ||||
|  | ||||
| type Reader interface { | ||||
| 	Read() (img image.Image, release func(), err error) | ||||
| 	Read() (img image.Image, err error) | ||||
| } | ||||
|  | ||||
| type ReaderFunc func() (img image.Image, release func(), err error) | ||||
| type ReaderFunc func() (img image.Image, err error) | ||||
|  | ||||
| func (rf ReaderFunc) Read() (img image.Image, release func(), err error) { | ||||
| 	img, release, err = rf() | ||||
| 	return | ||||
| func (rf ReaderFunc) Read() (img image.Image, err error) { | ||||
| 	return rf() | ||||
| } | ||||
|  | ||||
| // TransformFunc produces a new Reader that will produces a transformed video | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| package prop | ||||
|  | ||||
| import "fmt" | ||||
|  | ||||
| // BoolConstraint is an interface to represent bool value constraint. | ||||
| type BoolConstraint interface { | ||||
| 	Compare(bool) (float64, bool) | ||||
| @@ -22,11 +20,6 @@ func (b BoolExact) Compare(o bool) (float64, bool) { | ||||
| // Value implements BoolConstraint. | ||||
| func (b BoolExact) Value() bool { return bool(b) } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (b BoolExact) String() string { | ||||
| 	return fmt.Sprintf("%t (exact)", b) | ||||
| } | ||||
|  | ||||
| // Bool specifies ideal bool value. | ||||
| type Bool BoolExact | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| package prop | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| @@ -25,11 +23,6 @@ func (d Duration) Compare(a time.Duration) (float64, bool) { | ||||
| // Value implements DurationConstraint. | ||||
| func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (d Duration) String() string { | ||||
| 	return fmt.Sprintf("%v (ideal)", time.Duration(d)) | ||||
| } | ||||
|  | ||||
| // DurationExact specifies exact duration value. | ||||
| type DurationExact time.Duration | ||||
|  | ||||
| @@ -44,11 +37,6 @@ func (d DurationExact) Compare(a time.Duration) (float64, bool) { | ||||
| // Value implements DurationConstraint. | ||||
| func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (d DurationExact) String() string { | ||||
| 	return fmt.Sprintf("%v (exact)", time.Duration(d)) | ||||
| } | ||||
|  | ||||
| // DurationOneOf specifies list of expected duration values. | ||||
| type DurationOneOf []time.Duration | ||||
|  | ||||
| @@ -65,16 +53,6 @@ func (d DurationOneOf) Compare(a time.Duration) (float64, bool) { | ||||
| // Value implements DurationConstraint. | ||||
| func (DurationOneOf) Value() (time.Duration, bool) { return 0, false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (d DurationOneOf) String() string { | ||||
| 	var opts []string | ||||
| 	for _, v := range d { | ||||
| 		opts = append(opts, fmt.Sprint(v)) | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s (one of values)", strings.Join(opts, ",")) | ||||
| } | ||||
|  | ||||
| // DurationRanged specifies range of expected duration value. | ||||
| // If Ideal is non-zero, closest value to Ideal takes priority. | ||||
| type DurationRanged struct { | ||||
| @@ -118,8 +96,3 @@ func (d DurationRanged) Compare(a time.Duration) (float64, bool) { | ||||
|  | ||||
| // Value implements DurationConstraint. | ||||
| func (DurationRanged) Value() (time.Duration, bool) { return 0, false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (d DurationRanged) String() string { | ||||
| 	return fmt.Sprintf("%s - %s (range), %s (ideal)", d.Min, d.Max, d.Ideal) | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| package prop | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // FloatConstraint is an interface to represent float value constraint. | ||||
| @@ -24,11 +22,6 @@ func (f Float) Compare(a float32) (float64, bool) { | ||||
| // Value implements FloatConstraint. | ||||
| func (f Float) Value() (float32, bool) { return float32(f), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f Float) String() string { | ||||
| 	return fmt.Sprintf("%.2f (ideal)", f) | ||||
| } | ||||
|  | ||||
| // FloatExact specifies exact float value. | ||||
| type FloatExact float32 | ||||
|  | ||||
| @@ -43,11 +36,6 @@ func (f FloatExact) Compare(a float32) (float64, bool) { | ||||
| // Value implements FloatConstraint. | ||||
| func (f FloatExact) Value() (float32, bool) { return float32(f), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f FloatExact) String() string { | ||||
| 	return fmt.Sprintf("%.2f (exact)", f) | ||||
| } | ||||
|  | ||||
| // FloatOneOf specifies list of expected float values. | ||||
| type FloatOneOf []float32 | ||||
|  | ||||
| @@ -64,16 +52,6 @@ func (f FloatOneOf) Compare(a float32) (float64, bool) { | ||||
| // Value implements FloatConstraint. | ||||
| func (FloatOneOf) Value() (float32, bool) { return 0, false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f FloatOneOf) String() string { | ||||
| 	var opts []string | ||||
| 	for _, v := range f { | ||||
| 		opts = append(opts, fmt.Sprintf("%.2f", v)) | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s (one of values)", strings.Join(opts, ",")) | ||||
| } | ||||
|  | ||||
| // FloatRanged specifies range of expected float value. | ||||
| // If Ideal is non-zero, closest value to Ideal takes priority. | ||||
| type FloatRanged struct { | ||||
| @@ -117,8 +95,3 @@ func (f FloatRanged) Compare(a float32) (float64, bool) { | ||||
|  | ||||
| // Value implements FloatConstraint. | ||||
| func (FloatRanged) Value() (float32, bool) { return 0, false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f FloatRanged) String() string { | ||||
| 	return fmt.Sprintf("%.2f - %.2f (range), %.2f (ideal)", f.Min, f.Max, f.Ideal) | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| package prop | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/pion/mediadevices/pkg/frame" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // FrameFormatConstraint is an interface to represent frame format constraint. | ||||
| @@ -27,11 +25,6 @@ func (f FrameFormat) Compare(a frame.Format) (float64, bool) { | ||||
| // Value implements FrameFormatConstraint. | ||||
| func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f FrameFormat) String() string { | ||||
| 	return fmt.Sprintf("%s (ideal)", frame.Format(f)) | ||||
| } | ||||
|  | ||||
| // FrameFormatExact specifies exact frame format. | ||||
| type FrameFormatExact frame.Format | ||||
|  | ||||
| @@ -46,11 +39,6 @@ func (f FrameFormatExact) Compare(a frame.Format) (float64, bool) { | ||||
| // Value implements FrameFormatConstraint. | ||||
| func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f FrameFormatExact) String() string { | ||||
| 	return fmt.Sprintf("%s (exact)", frame.Format(f)) | ||||
| } | ||||
|  | ||||
| // FrameFormatOneOf specifies list of expected frame format. | ||||
| type FrameFormatOneOf []frame.Format | ||||
|  | ||||
| @@ -66,13 +54,3 @@ func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) { | ||||
|  | ||||
| // Value implements FrameFormatConstraint. | ||||
| func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f FrameFormatOneOf) String() string { | ||||
| 	var opts []string | ||||
| 	for _, v := range f { | ||||
| 		opts = append(opts, fmt.Sprint(v)) | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s (one of values)", strings.Join(opts, ",")) | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| package prop | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // IntConstraint is an interface to represent integer value constraint. | ||||
| @@ -24,11 +22,6 @@ func (i Int) Compare(a int) (float64, bool) { | ||||
| // Value implements IntConstraint. | ||||
| func (i Int) Value() (int, bool) { return int(i), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (i Int) String() string { | ||||
| 	return fmt.Sprintf("%d (ideal)", i) | ||||
| } | ||||
|  | ||||
| // IntExact specifies exact int value. | ||||
| type IntExact int | ||||
|  | ||||
| @@ -40,11 +33,6 @@ func (i IntExact) Compare(a int) (float64, bool) { | ||||
| 	return 1.0, false | ||||
| } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (i IntExact) String() string { | ||||
| 	return fmt.Sprintf("%d (exact)", i) | ||||
| } | ||||
|  | ||||
| // Value implements IntConstraint. | ||||
| func (i IntExact) Value() (int, bool) { return int(i), true } | ||||
|  | ||||
| @@ -64,16 +52,6 @@ func (i IntOneOf) Compare(a int) (float64, bool) { | ||||
| // Value implements IntConstraint. | ||||
| func (IntOneOf) Value() (int, bool) { return 0, false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (i IntOneOf) String() string { | ||||
| 	var opts []string | ||||
| 	for _, v := range i { | ||||
| 		opts = append(opts, fmt.Sprint(v)) | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Sprintf("%s (one of values)", strings.Join(opts, ",")) | ||||
| } | ||||
|  | ||||
| // IntRanged specifies range of expected int value. | ||||
| // If Ideal is non-zero, closest value to Ideal takes priority. | ||||
| type IntRanged struct { | ||||
| @@ -117,8 +95,3 @@ func (i IntRanged) Compare(a int) (float64, bool) { | ||||
|  | ||||
| // Value implements IntConstraint. | ||||
| func (IntRanged) Value() (int, bool) { return 0, false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (i IntRanged) String() string { | ||||
| 	return fmt.Sprintf("%d - %d (range), %d (ideal)", i.Min, i.Max, i.Ideal) | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,7 @@ | ||||
| package prop | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/frame" | ||||
| @@ -17,10 +15,6 @@ type MediaConstraints struct { | ||||
| 	AudioConstraints | ||||
| } | ||||
|  | ||||
| func (m *MediaConstraints) String() string { | ||||
| 	return prettifyStruct(m) | ||||
| } | ||||
|  | ||||
| // Media stores single set of media propaties. | ||||
| type Media struct { | ||||
| 	DeviceID string | ||||
| @@ -28,40 +22,6 @@ type Media struct { | ||||
| 	Audio | ||||
| } | ||||
|  | ||||
| func (m *Media) String() string { | ||||
| 	return prettifyStruct(m) | ||||
| } | ||||
|  | ||||
| func prettifyStruct(i interface{}) string { | ||||
| 	var rows []string | ||||
| 	var addRows func(int, reflect.Value) | ||||
| 	addRows = func(level int, obj reflect.Value) { | ||||
| 		typeOf := obj.Type() | ||||
| 		for i := 0; i < obj.NumField(); i++ { | ||||
| 			field := typeOf.Field(i) | ||||
| 			value := obj.Field(i) | ||||
|  | ||||
| 			padding := strings.Repeat("  ", level) | ||||
| 			switch value.Kind() { | ||||
| 			case reflect.Struct: | ||||
| 				rows = append(rows, fmt.Sprintf("%s%v:", padding, field.Name)) | ||||
| 				addRows(level+1, value) | ||||
| 			case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: | ||||
| 				if value.IsNil() { | ||||
| 					rows = append(rows, fmt.Sprintf("%s%v: any", padding, field.Name)) | ||||
| 				} else { | ||||
| 					rows = append(rows, fmt.Sprintf("%s%v: %v", padding, field.Name, value)) | ||||
| 				} | ||||
| 			default: | ||||
| 				rows = append(rows, fmt.Sprintf("%s%v: %v", padding, field.Name, value)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	addRows(0, reflect.ValueOf(i).Elem()) | ||||
| 	return strings.Join(rows, "\n") | ||||
| } | ||||
|  | ||||
| // setterFn is a callback function to set value from fieldB to fieldA | ||||
| type setterFn func(fieldA, fieldB reflect.Value) | ||||
|  | ||||
|   | ||||
| @@ -309,60 +309,3 @@ func TestMergeConstraintsNested(t *testing.T) { | ||||
| 		t.Error("expected a.Width to be 100, but got 0") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestString(t *testing.T) { | ||||
| 	t.Run("IdealValues", func(t *testing.T) { | ||||
| 		t.Log("\n", &MediaConstraints{ | ||||
| 			DeviceID: String("one"), | ||||
| 			VideoConstraints: VideoConstraints{ | ||||
| 				Width:       Int(1920), | ||||
| 				FrameRate:   Float(30.0), | ||||
| 				FrameFormat: FrameFormat(frame.FormatI420), | ||||
| 			}, | ||||
| 			AudioConstraints: AudioConstraints{ | ||||
| 				Latency: Duration(time.Millisecond * 20), | ||||
| 			}, | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("ExactValues", func(t *testing.T) { | ||||
| 		t.Log("\n", &MediaConstraints{ | ||||
| 			DeviceID: StringExact("one"), | ||||
| 			VideoConstraints: VideoConstraints{ | ||||
| 				Width:       IntExact(1920), | ||||
| 				FrameRate:   FloatExact(30.0), | ||||
| 				FrameFormat: FrameFormatExact(frame.FormatI420), | ||||
| 			}, | ||||
| 			AudioConstraints: AudioConstraints{ | ||||
| 				Latency:     DurationExact(time.Millisecond * 20), | ||||
| 				IsBigEndian: BoolExact(true), | ||||
| 			}, | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("OneOfValues", func(t *testing.T) { | ||||
| 		t.Log("\n", &MediaConstraints{ | ||||
| 			DeviceID: StringOneOf{"one", "two"}, | ||||
| 			VideoConstraints: VideoConstraints{ | ||||
| 				Width:       IntOneOf{1920, 1080}, | ||||
| 				FrameRate:   FloatOneOf{30.0, 60.1234}, | ||||
| 				FrameFormat: FrameFormatOneOf{frame.FormatI420, frame.FormatI444}, | ||||
| 			}, | ||||
| 			AudioConstraints: AudioConstraints{ | ||||
| 				Latency: DurationOneOf{time.Millisecond * 20, time.Millisecond * 40}, | ||||
| 			}, | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("RangedValues", func(t *testing.T) { | ||||
| 		t.Log("\n", &MediaConstraints{ | ||||
| 			VideoConstraints: VideoConstraints{ | ||||
| 				Width:     &IntRanged{Min: 1080, Max: 1920, Ideal: 1500}, | ||||
| 				FrameRate: &FloatRanged{Min: 30.123, Max: 60.12321312, Ideal: 45.12312312}, | ||||
| 			}, | ||||
| 			AudioConstraints: AudioConstraints{ | ||||
| 				Latency: &DurationRanged{Min: time.Millisecond * 20, Max: time.Millisecond * 40, Ideal: time.Millisecond * 30}, | ||||
| 			}, | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,5 @@ | ||||
| package prop | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // StringConstraint is an interface to represent string constraint. | ||||
| type StringConstraint interface { | ||||
| 	Compare(string) (float64, bool) | ||||
| @@ -26,11 +21,6 @@ func (f String) Compare(a string) (float64, bool) { | ||||
| // Value implements StringConstraint. | ||||
| func (f String) Value() (string, bool) { return string(f), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f String) String() string { | ||||
| 	return fmt.Sprintf("%s (ideal)", string(f)) | ||||
| } | ||||
|  | ||||
| // StringExact specifies exact string. | ||||
| type StringExact string | ||||
|  | ||||
| @@ -45,11 +35,6 @@ func (f StringExact) Compare(a string) (float64, bool) { | ||||
| // Value implements StringConstraint. | ||||
| func (f StringExact) Value() (string, bool) { return string(f), true } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f StringExact) String() string { | ||||
| 	return fmt.Sprintf("%s (exact)", string(f)) | ||||
| } | ||||
|  | ||||
| // StringOneOf specifies list of expected string. | ||||
| type StringOneOf []string | ||||
|  | ||||
| @@ -65,8 +50,3 @@ func (f StringOneOf) Compare(a string) (float64, bool) { | ||||
|  | ||||
| // Value implements StringConstraint. | ||||
| func (StringOneOf) Value() (string, bool) { return "", false } | ||||
|  | ||||
| // String implements Stringify | ||||
| func (f StringOneOf) String() string { | ||||
| 	return fmt.Sprintf("%s (one of values)", strings.Join([]string(f), ",")) | ||||
| } | ||||
|   | ||||
| @@ -1,149 +0,0 @@ | ||||
| package wave | ||||
|  | ||||
| import "fmt" | ||||
|  | ||||
| var ( | ||||
| 	errUnsupportedFormat = fmt.Errorf("Unsupported format") | ||||
| ) | ||||
|  | ||||
| // Buffer is a buffer that can store any audio format. | ||||
| type Buffer struct { | ||||
| 	// TODO: Probably standardize the audio formats so that we don't need to have the following different types | ||||
| 	//       and duplicated codes for each type | ||||
| 	bufferFloat32Interleaved    []float32 | ||||
| 	bufferFloat32NonInterleaved [][]float32 | ||||
| 	bufferInt16Interleaved      []int16 | ||||
| 	bufferInt16NonInterleaved   [][]int16 | ||||
| 	tmp                         Audio | ||||
| } | ||||
|  | ||||
| // NewBuffer creates a new Buffer instance | ||||
| func NewBuffer() *Buffer { | ||||
| 	return &Buffer{} | ||||
| } | ||||
|  | ||||
| // Load loads the current owned Audio | ||||
| func (buff *Buffer) Load() Audio { | ||||
| 	return buff.tmp | ||||
| } | ||||
|  | ||||
| // StoreCopy makes a copy of src and store its copy. StoreCopy will reuse as much memory as it can | ||||
| // from the previous copies. For example, if StoreCopy is given an audio that has the format from the previous call, | ||||
| // StoreCopy will not allocate extra memory and only copy the content from src to the previous buffer. | ||||
| func (buff *Buffer) StoreCopy(src Audio) { | ||||
| 	switch src := src.(type) { | ||||
| 	case *Float32Interleaved: | ||||
| 		clone, ok := buff.tmp.(*Float32Interleaved) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		neededSize := len(src.Data) | ||||
| 		if len(buff.bufferFloat32Interleaved) < neededSize { | ||||
| 			if cap(buff.bufferFloat32Interleaved) >= neededSize { | ||||
| 				buff.bufferFloat32Interleaved = buff.bufferFloat32Interleaved[:neededSize] | ||||
| 			} else { | ||||
| 				buff.bufferFloat32Interleaved = make([]float32, neededSize) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		copy(buff.bufferFloat32Interleaved, src.Data) | ||||
| 		clone.Data = buff.bufferFloat32Interleaved | ||||
| 		buff.tmp = clone | ||||
|  | ||||
| 	case *Float32NonInterleaved: | ||||
| 		clone, ok := buff.tmp.(*Float32NonInterleaved) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		neededSize := len(src.Data) | ||||
| 		if len(buff.bufferFloat32NonInterleaved) < neededSize { | ||||
| 			if cap(buff.bufferFloat32NonInterleaved) >= neededSize { | ||||
| 				buff.bufferFloat32NonInterleaved = buff.bufferFloat32NonInterleaved[:neededSize] | ||||
| 			} else { | ||||
| 				buff.bufferFloat32NonInterleaved = make([][]float32, neededSize) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for i := range src.Data { | ||||
| 			neededSize := len(src.Data[i]) | ||||
| 			if len(buff.bufferFloat32NonInterleaved[i]) < neededSize { | ||||
| 				if cap(buff.bufferFloat32NonInterleaved[i]) >= neededSize { | ||||
| 					buff.bufferFloat32NonInterleaved[i] = buff.bufferFloat32NonInterleaved[i][:neededSize] | ||||
| 				} else { | ||||
| 					buff.bufferFloat32NonInterleaved[i] = make([]float32, neededSize) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			copy(buff.bufferFloat32NonInterleaved[i], src.Data[i]) | ||||
| 		} | ||||
| 		clone.Data = buff.bufferFloat32NonInterleaved | ||||
| 		buff.tmp = clone | ||||
|  | ||||
| 	case *Int16Interleaved: | ||||
| 		clone, ok := buff.tmp.(*Int16Interleaved) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		neededSize := len(src.Data) | ||||
| 		if len(buff.bufferInt16Interleaved) < neededSize { | ||||
| 			if cap(buff.bufferInt16Interleaved) >= neededSize { | ||||
| 				buff.bufferInt16Interleaved = buff.bufferInt16Interleaved[:neededSize] | ||||
| 			} else { | ||||
| 				buff.bufferInt16Interleaved = make([]int16, neededSize) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		copy(buff.bufferInt16Interleaved, src.Data) | ||||
| 		clone.Data = buff.bufferInt16Interleaved | ||||
| 		buff.tmp = clone | ||||
|  | ||||
| 	case *Int16NonInterleaved: | ||||
| 		clone, ok := buff.tmp.(*Int16NonInterleaved) | ||||
| 		if ok { | ||||
| 			*clone = *src | ||||
| 		} else { | ||||
| 			copied := *src | ||||
| 			clone = &copied | ||||
| 		} | ||||
|  | ||||
| 		neededSize := len(src.Data) | ||||
| 		if len(buff.bufferInt16NonInterleaved) < neededSize { | ||||
| 			if cap(buff.bufferInt16NonInterleaved) >= neededSize { | ||||
| 				buff.bufferInt16NonInterleaved = buff.bufferInt16NonInterleaved[:neededSize] | ||||
| 			} else { | ||||
| 				buff.bufferInt16NonInterleaved = make([][]int16, neededSize) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for i := range src.Data { | ||||
| 			neededSize := len(src.Data[i]) | ||||
| 			if len(buff.bufferInt16NonInterleaved[i]) < neededSize { | ||||
| 				if cap(buff.bufferInt16NonInterleaved[i]) >= neededSize { | ||||
| 					buff.bufferInt16NonInterleaved[i] = buff.bufferInt16NonInterleaved[i][:neededSize] | ||||
| 				} else { | ||||
| 					buff.bufferInt16NonInterleaved[i] = make([]int16, neededSize) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			copy(buff.bufferInt16NonInterleaved[i], src.Data[i]) | ||||
| 		} | ||||
| 		clone.Data = buff.bufferInt16NonInterleaved | ||||
| 		buff.tmp = clone | ||||
|  | ||||
| 	default: | ||||
| 		// TODO: Should have a routine to convert any format to one of the supported formats above | ||||
| 		panic(errUnsupportedFormat) | ||||
| 	} | ||||
| } | ||||
| @@ -1,129 +0,0 @@ | ||||
| package wave | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	errIdenticalAddress = errors.New("Cloned audio has the same memory address with the original audio") | ||||
| ) | ||||
|  | ||||
| func TestBufferStoreCopyAndLoad(t *testing.T) { | ||||
| 	chunkInfo := ChunkInfo{ | ||||
| 		Len:          4, | ||||
| 		Channels:     2, | ||||
| 		SamplingRate: 48000, | ||||
| 	} | ||||
| 	testCases := map[string]struct { | ||||
| 		New      func() EditableAudio | ||||
| 		Update   func(EditableAudio) | ||||
| 		Validate func(*testing.T, Audio, Audio) | ||||
| 	}{ | ||||
| 		"Float32Interleaved": { | ||||
| 			New: func() EditableAudio { | ||||
| 				return NewFloat32Interleaved(chunkInfo) | ||||
| 			}, | ||||
| 			Update: func(src EditableAudio) { | ||||
| 				src.Set(0, 0, Float32Sample(1)) | ||||
| 			}, | ||||
| 			Validate: func(t *testing.T, original Audio, clone Audio) { | ||||
| 				ok := reflect.ValueOf(original.(*Float32Interleaved).Data).Pointer() != reflect.ValueOf(clone.(*Float32Interleaved).Data).Pointer() | ||||
| 				if !ok { | ||||
| 					t.Error(errIdenticalAddress) | ||||
| 				} | ||||
| 			}, | ||||
| 		}, | ||||
| 		"Float32NonInterleaved": { | ||||
| 			New: func() EditableAudio { | ||||
| 				return NewFloat32NonInterleaved(chunkInfo) | ||||
| 			}, | ||||
| 			Update: func(src EditableAudio) { | ||||
| 				src.Set(0, 0, Float32Sample(1)) | ||||
| 			}, | ||||
| 			Validate: func(t *testing.T, original Audio, clone Audio) { | ||||
| 				originalReal := original.(*Float32NonInterleaved) | ||||
| 				cloneReal := clone.(*Float32NonInterleaved) | ||||
| 				if reflect.ValueOf(originalReal.Data).Pointer() == reflect.ValueOf(cloneReal.Data).Pointer() { | ||||
| 					t.Error(errIdenticalAddress) | ||||
| 				} | ||||
|  | ||||
| 				for i := range cloneReal.Data { | ||||
| 					if reflect.ValueOf(originalReal.Data[i]).Pointer() == reflect.ValueOf(cloneReal.Data[i]).Pointer() { | ||||
| 						err := fmt.Errorf("Channel %d memory address should be different", i) | ||||
| 						t.Errorf("%v: %w", errIdenticalAddress, err) | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 		}, | ||||
| 		"Int16Interleaved": { | ||||
| 			New: func() EditableAudio { | ||||
| 				return NewInt16Interleaved(chunkInfo) | ||||
| 			}, | ||||
| 			Update: func(src EditableAudio) { | ||||
| 				src.Set(1, 1, Int16Sample(2)) | ||||
| 			}, | ||||
| 			Validate: func(t *testing.T, original Audio, clone Audio) { | ||||
| 				ok := reflect.ValueOf(original.(*Int16Interleaved).Data).Pointer() != reflect.ValueOf(clone.(*Int16Interleaved).Data).Pointer() | ||||
| 				if !ok { | ||||
| 					t.Error(errIdenticalAddress) | ||||
| 				} | ||||
| 			}, | ||||
| 		}, | ||||
| 		"Int16NonInterleaved": { | ||||
| 			New: func() EditableAudio { | ||||
| 				return NewInt16NonInterleaved(chunkInfo) | ||||
| 			}, | ||||
| 			Update: func(src EditableAudio) { | ||||
| 				src.Set(1, 1, Int16Sample(2)) | ||||
| 			}, | ||||
| 			Validate: func(t *testing.T, original Audio, clone Audio) { | ||||
| 				originalReal := original.(*Int16NonInterleaved) | ||||
| 				cloneReal := clone.(*Int16NonInterleaved) | ||||
| 				if reflect.ValueOf(originalReal.Data).Pointer() == reflect.ValueOf(cloneReal.Data).Pointer() { | ||||
| 					t.Error(errIdenticalAddress) | ||||
| 				} | ||||
|  | ||||
| 				for i := range cloneReal.Data { | ||||
| 					if reflect.ValueOf(originalReal.Data[i]).Pointer() == reflect.ValueOf(cloneReal.Data[i]).Pointer() { | ||||
| 						err := fmt.Errorf("Channel %d memory address should be different", i) | ||||
| 						t.Errorf("%v: %w", errIdenticalAddress, err) | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	buffer := NewBuffer() | ||||
|  | ||||
| 	for name, testCase := range testCases { | ||||
| 		// Since the test also wants to make sure that Copier can convert from 1 type to another, | ||||
| 		// t.Run is not ideal since it'll run the tests separately | ||||
| 		t.Log("Testing", name) | ||||
|  | ||||
| 		src := testCase.New() | ||||
| 		src.Set(0, 0, Int16Sample(1)) | ||||
| 		buffer.StoreCopy(src) | ||||
|  | ||||
| 		testCase.Validate(t, src, buffer.Load()) | ||||
|  | ||||
| 		if !reflect.DeepEqual(buffer.Load(), src) { | ||||
| 			t.Fatalf(`Expected the copied audio chunk to be identical with the source | ||||
|  | ||||
| Expected: | ||||
| %v | ||||
|  | ||||
| Actual: | ||||
| %v | ||||
| 			`, src, buffer.Load()) | ||||
| 		} | ||||
|  | ||||
| 		testCase.Update(src) | ||||
| 		buffer.StoreCopy(src) | ||||
| 		if !reflect.DeepEqual(buffer.Load(), src) { | ||||
| 			t.Fatal("Expected the copied audio chunk to be identical with the source after an update in source") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										101
									
								
								rtp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								rtp.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/prop" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/rtcp" | ||||
| 	"github.com/pion/rtp" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| type RTPTracker struct { | ||||
| 	videoEncoders []codec.VideoEncoderBuilder | ||||
| 	audioEncoders []codec.AudioEncoderBuilder | ||||
| } | ||||
|  | ||||
| type RTPTrackerOption func(*RTPTracker) | ||||
|  | ||||
| func WithVideoEncoders(codecs ...codec.VideoEncoderBuilder) func(*RTPTracker) { | ||||
| 	return func(tracker *RTPTracker) { | ||||
| 		tracker.videoEncoders = codecs | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func WithAudioEncoders(codecs ...codec.AudioEncoderBuilder) func(*RTPTracker) { | ||||
| 	return func(tracker *RTPTracker) { | ||||
| 		tracker.audioEncoders = codecs | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func NewRTPTracker(opts ...RTPTrackerOption) *RTPTracker { | ||||
| 	var tracker RTPTracker | ||||
|  | ||||
| 	for _, opt := range opts { | ||||
| 		opt(&tracker) | ||||
| 	} | ||||
|  | ||||
| 	return &tracker | ||||
| } | ||||
|  | ||||
| func (tracker *RTPTracker) Track(track Track) *RTPTrack { | ||||
| 	rtpTrack := RTPTrack{ | ||||
| 		Track: track, | ||||
| 	} | ||||
|  | ||||
| 	return &rtpTrack | ||||
| } | ||||
|  | ||||
| type RTPTrack struct { | ||||
| 	Track | ||||
| 	tracker        *RTPTracker | ||||
| 	currentEncoder codec.ReadCloser | ||||
| 	currentParams  RTPParameters | ||||
| 	lastProp       prop.Media | ||||
| } | ||||
|  | ||||
| func (track *RTPTrack) SetParameters(params RTPParameters) error { | ||||
| 	var err error | ||||
|  | ||||
| 	switch t := track.Track.(type) { | ||||
| 	case *VideoTrack: | ||||
| 		err = track.setParametersVideo(t, ¶ms) | ||||
| 	case *AudioTrack: | ||||
| 		err = track.setParametersAudio(t, ¶ms) | ||||
| 	default: | ||||
| 		err = fmt.Errorf("unsupported track type") | ||||
| 	} | ||||
|  | ||||
| 	if err == nil { | ||||
| 		track.currentParams = params | ||||
| 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (track *RTPTrack) setParametersVideo(videoTrack *VideoTrack, params *RTPParameters) error { | ||||
| 	if params.SelectedCodec.Type != webrtc.RTPCodecTypeVideo { | ||||
| 		return fmt.Errorf("invalid selected RTP codec type. Expected video but got audio") | ||||
| 	} | ||||
|  | ||||
| 	video.DetectChanges(interval time.Duration, onChange func(prop.Media)) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (track *RTPTrack) setParametersAudio(audioTrack *AudioTrack, params *RTPParameters) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (track *RTPTrack) ReadRTP() (*rtp.Packet, error) { | ||||
| 	if track.currentEncoder == nil { | ||||
| 		return nil, fmt.Errorf("Encoder has not been specified. Please call SetParameters to specify.") | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (track *RTPTrack) WriteRTCP(packet rtcp.Packet) error { | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										36
									
								
								sampler.go
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								sampler.go
									
									
									
									
									
								
							| @@ -1,36 +0,0 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"math" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| 	"github.com/pion/webrtc/v2/pkg/media" | ||||
| ) | ||||
|  | ||||
| type samplerFunc func(b []byte) error | ||||
|  | ||||
| // newVideoSampler creates a video sampler that uses the actual video frame rate and | ||||
| // the codec's clock rate to come up with a duration for each sample. | ||||
| func newVideoSampler(t *webrtc.Track) samplerFunc { | ||||
| 	clockRate := float64(t.Codec().ClockRate) | ||||
| 	lastTimestamp := time.Now() | ||||
|  | ||||
| 	return samplerFunc(func(b []byte) error { | ||||
| 		now := time.Now() | ||||
| 		duration := now.Sub(lastTimestamp).Seconds() | ||||
| 		samples := uint32(math.Round(clockRate * duration)) | ||||
| 		lastTimestamp = now | ||||
|  | ||||
| 		return t.WriteSample(media.Sample{Data: b, Samples: samples}) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // newAudioSampler creates a audio sampler that uses a fixed latency and | ||||
| // the codec's clock rate to come up with a duration for each sample. | ||||
| func newAudioSampler(t *webrtc.Track, latency time.Duration) samplerFunc { | ||||
| 	samples := uint32(math.Round(float64(t.Codec().ClockRate) * latency.Seconds())) | ||||
| 	return samplerFunc(func(b []byte) error { | ||||
| 		return t.WriteSample(media.Sample{Data: b, Samples: samples}) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										470
									
								
								track.go
									
									
									
									
									
								
							
							
						
						
									
										470
									
								
								track.go
									
									
									
									
									
								
							| @@ -1,325 +1,245 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"image" | ||||
| 	"math/rand" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/pion/mediadevices/pkg/codec" | ||||
| 	"github.com/pion/mediadevices/pkg/driver" | ||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | ||||
| 	"github.com/pion/mediadevices/pkg/io/video" | ||||
| 	"github.com/pion/mediadevices/pkg/wave" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	errInvalidDriverType      = errors.New("invalid driver type") | ||||
| 	errNotFoundPeerConnection = errors.New("failed to find given peer connection") | ||||
| // TrackKind represents content type of a track | ||||
| type TrackKind string | ||||
|  | ||||
| const ( | ||||
| 	TrackKindVideo TrackKind = "video" | ||||
| 	TrackKindAudio TrackKind = "audio" | ||||
| ) | ||||
|  | ||||
| // Source is a generic representation of a media source | ||||
| type Source interface { | ||||
| 	ID() string | ||||
| 	Close() error | ||||
| } | ||||
|  | ||||
| // VideoSource is a specific type of media source that emits a series of video frames | ||||
| type VideoSource interface { | ||||
| 	video.Reader | ||||
| 	Source | ||||
| } | ||||
|  | ||||
| // AudioSource is a specific type of media source that emits a series of audio chunks | ||||
| type AudioSource interface { | ||||
| 	audio.Reader | ||||
| 	Source | ||||
| } | ||||
|  | ||||
| // Track is an interface that represent MediaStreamTrack | ||||
| // Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack | ||||
| type Track interface { | ||||
| 	Source | ||||
| 	ID() string | ||||
| 	Kind() TrackKind | ||||
| 	Stop() | ||||
| 	// OnEnded registers a handler to receive an error from the media stream track. | ||||
| 	// If the error is already occured before registering, the handler will be | ||||
| 	// immediately called. | ||||
| 	OnEnded(func(error)) | ||||
| 	Kind() MediaDeviceType | ||||
| 	// Bind binds the current track source to the given peer connection. In Pion/webrtc v3, the bind | ||||
| 	// call will happen automatically after the SDP negotiation. Users won't need to call this manually. | ||||
| 	Bind(*webrtc.PeerConnection) (*webrtc.Track, error) | ||||
| 	// Unbind is the clean up operation that should be called after Bind. Similar to Bind, unbind will | ||||
| 	// be called automatically in the future. | ||||
| 	Unbind(*webrtc.PeerConnection) error | ||||
| } | ||||
|  | ||||
| type baseTrack struct { | ||||
| 	Source | ||||
| 	err                   error | ||||
| 	onErrorHandler        func(error) | ||||
| 	mu                    sync.Mutex | ||||
| 	endOnce               sync.Once | ||||
| 	kind                  MediaDeviceType | ||||
| 	selector              *CodecSelector | ||||
| 	activePeerConnections map[*webrtc.PeerConnection]chan<- chan<- struct{} | ||||
| // VideoTrack is a specialized track for video | ||||
| type VideoTrack struct { | ||||
| 	baseTrack | ||||
| 	src         video.Reader | ||||
| 	transformed video.Reader | ||||
| 	mux         sync.Mutex | ||||
| 	frameCount  int | ||||
| 	lastFrame   image.Image | ||||
| 	lastErr     error | ||||
| } | ||||
|  | ||||
| func newBaseTrack(source Source, kind MediaDeviceType, selector *CodecSelector) *baseTrack { | ||||
| 	return &baseTrack{ | ||||
| 		Source:                source, | ||||
| 		kind:                  kind, | ||||
| 		selector:              selector, | ||||
| 		activePeerConnections: make(map[*webrtc.PeerConnection]chan<- chan<- struct{}), | ||||
| func newVideoTrack(d driver.Driver, constraints MediaTrackConstraints) (*VideoTrack, error) { | ||||
| 	err := d.Open() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	recorder, ok := d.(driver.VideoRecorder) | ||||
| 	if !ok { | ||||
| 		d.Close() | ||||
| 		return nil, fmt.Errorf("driver is not an video recorder") | ||||
| 	} | ||||
|  | ||||
| 	r, err := recorder.VideoRecord(constraints.selectedMedia) | ||||
| 	if err != nil { | ||||
| 		d.Close() | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &VideoTrack{ | ||||
| 		baseTrack:   newBaseTrack(d, constraints), | ||||
| 		src:         r, | ||||
| 		transformed: r, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Kind returns track's kind | ||||
| func (track *baseTrack) Kind() MediaDeviceType { | ||||
| 	return track.kind | ||||
| func (track *VideoTrack) Kind() TrackKind { | ||||
| 	return TrackKindVideo | ||||
| } | ||||
|  | ||||
| // NewReader returns a reader to read frames from the source. You may create multiple | ||||
| // readers and read from them in different goroutines. | ||||
| // | ||||
| // In the case of multiple readers, reading from the source will only get triggered | ||||
| // when the reader has the latest frame from the source | ||||
| func (track *VideoTrack) NewReader() video.Reader { | ||||
| 	var curFrameCount int | ||||
| 	return video.ReaderFunc(func() (img image.Image, err error) { | ||||
| 		track.mux.Lock() | ||||
| 		defer track.mux.Unlock() | ||||
|  | ||||
| 		if curFrameCount != track.frameCount { | ||||
| 			img = copyFrame(img, track.lastFrame) | ||||
| 			err = track.lastErr | ||||
| 		} else { | ||||
| 			img, err = track.transformed.Read() | ||||
| 			track.lastFrame = img | ||||
| 			track.lastErr = err | ||||
| 			track.frameCount++ | ||||
| 			if err != nil { | ||||
| 				track.onErrorHandler(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		curFrameCount = track.frameCount | ||||
| 		return | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // TODO: implement copy in place | ||||
| func copyFrame(dst, src image.Image) image.Image { return src } | ||||
|  | ||||
| // Transform transforms the underlying source. The transformation will reflect to | ||||
| // all readers | ||||
| func (track *VideoTrack) Transform(fns ...video.TransformFunc) { | ||||
| 	track.mux.Lock() | ||||
| 	defer track.mux.Unlock() | ||||
| 	track.transformed = video.Merge(fns...)(track.src) | ||||
| } | ||||
|  | ||||
| // AudioTrack is a specialized track for audio | ||||
| type AudioTrack struct { | ||||
| 	baseTrack | ||||
| 	src         audio.Reader | ||||
| 	transformed audio.Reader | ||||
| 	mux         sync.Mutex | ||||
| 	chunkCount  int | ||||
| 	lastChunks  wave.Audio | ||||
| 	lastErr     error | ||||
| } | ||||
|  | ||||
| func newAudioTrack(d driver.Driver, constraints MediaTrackConstraints) (*AudioTrack, error) { | ||||
| 	err := d.Open() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	recorder, ok := d.(driver.AudioRecorder) | ||||
| 	if !ok { | ||||
| 		d.Close() | ||||
| 		return nil, fmt.Errorf("driver is not an audio recorder") | ||||
| 	} | ||||
|  | ||||
| 	r, err := recorder.AudioRecord(constraints.selectedMedia) | ||||
| 	if err != nil { | ||||
| 		d.Close() | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &AudioTrack{ | ||||
| 		baseTrack:   newBaseTrack(d, constraints), | ||||
| 		src:         r, | ||||
| 		transformed: r, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (track *AudioTrack) Kind() TrackKind { | ||||
| 	return TrackKindAudio | ||||
| } | ||||
|  | ||||
| // NewReader returns a reader to read audio chunks from the source. You may create multiple | ||||
| // readers and read from them in different goroutines. | ||||
| // | ||||
| // In the case of multiple readers, reading from the source will only get triggered | ||||
| // when the reader has the latest chunk from the source | ||||
| func (track *AudioTrack) NewReader() audio.Reader { | ||||
| 	var currChunkCount int | ||||
| 	return audio.ReaderFunc(func() (chunks wave.Audio, err error) { | ||||
| 		track.mux.Lock() | ||||
| 		defer track.mux.Unlock() | ||||
|  | ||||
| 		if currChunkCount != track.chunkCount { | ||||
| 			chunks = copyChunks(chunks, track.lastChunks) | ||||
| 			err = track.lastErr | ||||
| 		} else { | ||||
| 			chunks, err = track.transformed.Read() | ||||
| 			track.lastChunks = chunks | ||||
| 			track.lastErr = err | ||||
| 			track.chunkCount++ | ||||
| 			if err != nil { | ||||
| 				track.onErrorHandler(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		currChunkCount = track.chunkCount | ||||
| 		return | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // TODO: implement copy in place | ||||
| func copyChunks(dst, src wave.Audio) wave.Audio { return src } | ||||
|  | ||||
| // Transform transforms the underlying source. The transformation will reflect to | ||||
| // all readers | ||||
| func (track *AudioTrack) Transform(fns ...audio.TransformFunc) { | ||||
| 	track.mux.Lock() | ||||
| 	defer track.mux.Unlock() | ||||
| 	track.transformed = audio.Merge(fns...)(track.src) | ||||
| } | ||||
|  | ||||
| type baseTrack struct { | ||||
| 	d           driver.Driver | ||||
| 	constraints MediaTrackConstraints | ||||
|  | ||||
| 	onErrorHandler func(error) | ||||
| 	err            error | ||||
| 	mu             sync.Mutex | ||||
| 	endOnce        sync.Once | ||||
| } | ||||
|  | ||||
| func newBaseTrack(d driver.Driver, constraints MediaTrackConstraints) baseTrack { | ||||
| 	return baseTrack{d: d, constraints: constraints} | ||||
| } | ||||
|  | ||||
| func (t *baseTrack) ID() string { | ||||
| 	return t.d.ID() | ||||
| } | ||||
|  | ||||
| // OnEnded sets an error handler. When a track has been created and started, if an | ||||
| // error occurs, handler will get called with the error given to the parameter. | ||||
| func (track *baseTrack) OnEnded(handler func(error)) { | ||||
| 	track.mu.Lock() | ||||
| 	track.onErrorHandler = handler | ||||
| 	err := track.err | ||||
| 	track.mu.Unlock() | ||||
| func (t *baseTrack) OnEnded(handler func(error)) { | ||||
| 	t.mu.Lock() | ||||
| 	t.onErrorHandler = handler | ||||
| 	err := t.err | ||||
| 	t.mu.Unlock() | ||||
|  | ||||
| 	if err != nil && handler != nil { | ||||
| 		// Already errored. | ||||
| 		track.endOnce.Do(func() { | ||||
| 		t.endOnce.Do(func() { | ||||
| 			handler(err) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // onError is a callback when an error occurs | ||||
| func (track *baseTrack) onError(err error) { | ||||
| 	track.mu.Lock() | ||||
| 	track.err = err | ||||
| 	handler := track.onErrorHandler | ||||
| 	track.mu.Unlock() | ||||
| func (t *baseTrack) onError(err error) { | ||||
| 	t.mu.Lock() | ||||
| 	t.err = err | ||||
| 	handler := t.onErrorHandler | ||||
| 	t.mu.Unlock() | ||||
|  | ||||
| 	if handler != nil { | ||||
| 		track.endOnce.Do(func() { | ||||
| 		t.endOnce.Do(func() { | ||||
| 			handler(err) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (track *baseTrack) bind(pc *webrtc.PeerConnection, encodedReader codec.ReadCloser, selectedCodec *codec.RTPCodec, sampler func(*webrtc.Track) samplerFunc) (*webrtc.Track, error) { | ||||
| 	track.mu.Lock() | ||||
| 	defer track.mu.Unlock() | ||||
|  | ||||
| 	webrtcTrack, err := pc.NewTrack(selectedCodec.PayloadType, rand.Uint32(), track.ID(), selectedCodec.MimeType) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	sample := sampler(webrtcTrack) | ||||
| 	signalCh := make(chan chan<- struct{}) | ||||
| 	track.activePeerConnections[pc] = signalCh | ||||
|  | ||||
| 	go func() { | ||||
| 		var doneCh chan<- struct{} | ||||
| 		defer func() { | ||||
| 			encodedReader.Close() | ||||
|  | ||||
| 			// When there's another call to unbind, it won't block since we mark the signalCh to be closed | ||||
| 			close(signalCh) | ||||
| 			if doneCh != nil { | ||||
| 				close(doneCh) | ||||
| 			} | ||||
| 		}() | ||||
|  | ||||
| 		for { | ||||
| 			select { | ||||
| 			case doneCh = <-signalCh: | ||||
| 				return | ||||
| 			default: | ||||
| 			} | ||||
|  | ||||
| 			buff, _, err := encodedReader.Read() | ||||
| 			if err != nil { | ||||
| 				track.onError(err) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err := sample(buff); err != nil { | ||||
| 				track.onError(err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	return webrtcTrack, nil | ||||
| } | ||||
|  | ||||
| func (track *baseTrack) unbind(pc *webrtc.PeerConnection) error { | ||||
| 	track.mu.Lock() | ||||
| 	defer track.mu.Unlock() | ||||
|  | ||||
| 	ch, ok := track.activePeerConnections[pc] | ||||
| 	if !ok { | ||||
| 		return errNotFoundPeerConnection | ||||
| 	} | ||||
|  | ||||
| 	doneCh := make(chan struct{}) | ||||
| 	ch <- doneCh | ||||
| 	<-doneCh | ||||
| 	delete(track.activePeerConnections, pc) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func newTrackFromDriver(d driver.Driver, constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | ||||
| 	if err := d.Open(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	switch recorder := d.(type) { | ||||
| 	case driver.VideoRecorder: | ||||
| 		return newVideoTrackFromDriver(d, recorder, constraints, selector) | ||||
| 	case driver.AudioRecorder: | ||||
| 		return newAudioTrackFromDriver(d, recorder, constraints, selector) | ||||
| 	default: | ||||
| 		panic(errInvalidDriverType) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // VideoTrack is a specific track type that contains video source which allows multiple readers to access, and manipulate. | ||||
| type VideoTrack struct { | ||||
| 	*baseTrack | ||||
| 	*video.Broadcaster | ||||
| } | ||||
|  | ||||
| // NewVideoTrack constructs a new VideoTrack | ||||
| func NewVideoTrack(source VideoSource, selector *CodecSelector) Track { | ||||
| 	return newVideoTrackFromReader(source, source, selector) | ||||
| } | ||||
|  | ||||
| func newVideoTrackFromReader(source Source, reader video.Reader, selector *CodecSelector) Track { | ||||
| 	base := newBaseTrack(source, VideoInput, selector) | ||||
| 	wrappedReader := video.ReaderFunc(func() (img image.Image, release func(), err error) { | ||||
| 		img, _, err = reader.Read() | ||||
| 		if err != nil { | ||||
| 			base.onError(err) | ||||
| 		} | ||||
| 		return img, func() {}, err | ||||
| 	}) | ||||
|  | ||||
| 	// TODO: Allow users to configure broadcaster | ||||
| 	broadcaster := video.NewBroadcaster(wrappedReader, nil) | ||||
|  | ||||
| 	return &VideoTrack{ | ||||
| 		baseTrack:   base, | ||||
| 		Broadcaster: broadcaster, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // newVideoTrackFromDriver is an internal video track creation from driver | ||||
| func newVideoTrackFromDriver(d driver.Driver, recorder driver.VideoRecorder, constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | ||||
| 	reader, err := recorder.VideoRecord(constraints.selectedMedia) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return newVideoTrackFromReader(d, reader, selector), nil | ||||
| } | ||||
|  | ||||
| // Transform transforms the underlying source by applying the given fns in serial order | ||||
| func (track *VideoTrack) Transform(fns ...video.TransformFunc) { | ||||
| 	src := track.Broadcaster.Source() | ||||
| 	track.Broadcaster.ReplaceSource(video.Merge(fns...)(src)) | ||||
| } | ||||
|  | ||||
| func (track *VideoTrack) Bind(pc *webrtc.PeerConnection) (*webrtc.Track, error) { | ||||
| 	reader := track.NewReader(false) | ||||
| 	inputProp, err := detectCurrentVideoProp(track.Broadcaster) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	wantCodecs := pc.GetRegisteredRTPCodecs(webrtc.RTPCodecTypeVideo) | ||||
| 	encodedReader, selectedCodec, err := track.selector.selectVideoCodec(wantCodecs, reader, inputProp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return track.bind(pc, encodedReader, selectedCodec, newVideoSampler) | ||||
| } | ||||
|  | ||||
| func (track *VideoTrack) Unbind(pc *webrtc.PeerConnection) error { | ||||
| 	return track.unbind(pc) | ||||
| } | ||||
|  | ||||
| // AudioTrack is a specific track type that contains audio source which allows multiple readers to access, and | ||||
| // manipulate. | ||||
| type AudioTrack struct { | ||||
| 	*baseTrack | ||||
| 	*audio.Broadcaster | ||||
| } | ||||
|  | ||||
| // NewAudioTrack constructs a new VideoTrack | ||||
| func NewAudioTrack(source AudioSource, selector *CodecSelector) Track { | ||||
| 	return newAudioTrackFromReader(source, source, selector) | ||||
| } | ||||
|  | ||||
| func newAudioTrackFromReader(source Source, reader audio.Reader, selector *CodecSelector) Track { | ||||
| 	base := newBaseTrack(source, AudioInput, selector) | ||||
| 	wrappedReader := audio.ReaderFunc(func() (chunk wave.Audio, release func(), err error) { | ||||
| 		chunk, _, err = reader.Read() | ||||
| 		if err != nil { | ||||
| 			base.onError(err) | ||||
| 		} | ||||
| 		return chunk, func() {}, err | ||||
| 	}) | ||||
|  | ||||
| 	// TODO: Allow users to configure broadcaster | ||||
| 	broadcaster := audio.NewBroadcaster(wrappedReader, nil) | ||||
|  | ||||
| 	return &AudioTrack{ | ||||
| 		baseTrack:   base, | ||||
| 		Broadcaster: broadcaster, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // newAudioTrackFromDriver is an internal audio track creation from driver | ||||
| func newAudioTrackFromDriver(d driver.Driver, recorder driver.AudioRecorder, constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | ||||
| 	reader, err := recorder.AudioRecord(constraints.selectedMedia) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// FIXME: The current audio detection and audio encoder can only work with a static latency. Since the latency from the driver | ||||
| 	//        can fluctuate, we need to stabilize it. Maybe there's a better way for doing this? | ||||
| 	reader = audio.NewBuffer(int(constraints.selectedMedia.Latency.Seconds() * float64(constraints.selectedMedia.SampleRate)))(reader) | ||||
| 	return newAudioTrackFromReader(d, reader, selector), nil | ||||
| } | ||||
|  | ||||
| // Transform transforms the underlying source by applying the given fns in serial order | ||||
| func (track *AudioTrack) Transform(fns ...audio.TransformFunc) { | ||||
| 	src := track.Broadcaster.Source() | ||||
| 	track.Broadcaster.ReplaceSource(audio.Merge(fns...)(src)) | ||||
| } | ||||
|  | ||||
| func (track *AudioTrack) Bind(pc *webrtc.PeerConnection) (*webrtc.Track, error) { | ||||
| 	reader := track.NewReader(false) | ||||
| 	inputProp, err := detectCurrentAudioProp(track.Broadcaster) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	wantCodecs := pc.GetRegisteredRTPCodecs(webrtc.RTPCodecTypeAudio) | ||||
| 	encodedReader, selectedCodec, err := track.selector.selectAudioCodec(wantCodecs, reader, inputProp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return track.bind(pc, encodedReader, selectedCodec, func(t *webrtc.Track) samplerFunc { return newAudioSampler(t, inputProp.Latency) }) | ||||
| } | ||||
|  | ||||
| func (track *AudioTrack) Unbind(pc *webrtc.PeerConnection) error { | ||||
| 	return track.unbind(pc) | ||||
| func (t *baseTrack) Stop() { | ||||
| 	t.d.Close() | ||||
| } | ||||
|   | ||||
							
								
								
									
										45
									
								
								webrtc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								webrtc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| package mediadevices | ||||
|  | ||||
| import ( | ||||
| 	"github.com/pion/rtcp" | ||||
| 	"github.com/pion/rtp" | ||||
| 	"github.com/pion/webrtc/v2" | ||||
| ) | ||||
|  | ||||
| // == WebRTC v3 design == | ||||
|  | ||||
| // Reader is an interface to handle incoming RTP stream. | ||||
| type Reader interface { | ||||
| 	ReadRTP() (*rtp.Packet, error) | ||||
| 	WriteRTCP(rtcp.Packet) error | ||||
| } | ||||
|  | ||||
| // TrackBase represents common MediaStreamTrack functionality of LocalTrack and RemoteTrack. | ||||
| type TrackBase interface { | ||||
| 	ID() string | ||||
| } | ||||
|  | ||||
| type LocalRTPTrack interface { | ||||
| 	TrackBase | ||||
| 	Reader | ||||
|  | ||||
| 	// SetParameters sets information about how the data is to be encoded. | ||||
| 	// This will be called by PeerConnection according to the result of | ||||
| 	// SDP based negotiation. | ||||
| 	// It will be called via RTPSender.Parameters() by PeerConnection to | ||||
| 	// tell the negotiated media codec information. | ||||
| 	// | ||||
| 	// This is pion's extension to process data without having encoder/decoder | ||||
| 	// in webrtc package. | ||||
| 	SetParameters(RTPParameters) error | ||||
| } | ||||
|  | ||||
| // RTPParameters represents RTCRtpParameters which contains information about | ||||
| // how the RTC data is to be encoded/decoded. | ||||
| // | ||||
| // ref: https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSendParameters | ||||
| type RTPParameters struct { | ||||
| 	SSRC          uint32 | ||||
| 	SelectedCodec *webrtc.RTPCodec | ||||
| 	Codecs        []*webrtc.RTPCodec | ||||
| } | ||||
		Reference in New Issue
	
	Block a user