mirror of
				https://github.com/pion/mediadevices.git
				synced 2025-10-31 20:02:36 +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 |     runs-on: ubuntu-latest | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         go: [ '1.15', '1.14' ] |         go: [ '1.14', '1.13' ] | ||||||
|     name: Linux Go ${{ matrix.go }} |     name: Linux Go ${{ matrix.go }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
|       - name: Setup Go |       - name: Setup Go | ||||||
|         uses: actions/setup-go@v2 |         uses: actions/setup-go@v1 | ||||||
|         with: |         with: | ||||||
|           go-version: ${{ matrix.go }} |           go-version: ${{ matrix.go }} | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
| @@ -30,17 +30,15 @@ jobs: | |||||||
|             libvpx-dev \ |             libvpx-dev \ | ||||||
|             libx264-dev |             libx264-dev | ||||||
|       - name: go vet |       - name: go vet | ||||||
|         run: go vet $(go list ./... | grep -v mmal) |         run: go vet ./... | ||||||
|       - name: go build |       - name: go build | ||||||
|         run: go build $(go list ./... | grep -v mmal) |         run: go build ./... | ||||||
|       - name: go build without CGO |       - name: go build without CGO | ||||||
|         run: go build . pkg/... |         run: go build . pkg/... | ||||||
|         env: |         env: | ||||||
|           CGO_ENABLED: 0 |           CGO_ENABLED: 0 | ||||||
|       - name: go test |       - name: go test | ||||||
|         run: go test -v -race -coverprofile=coverage.txt -covermode=atomic $(go list ./... | grep -v mmal) |         run: go test ./... -v -race | ||||||
|       - uses: codecov/codecov-action@v1 |  | ||||||
|         if: matrix.go == '1.15' |  | ||||||
|       - name: go test without CGO |       - name: go test without CGO | ||||||
|         run: go test . pkg/... -v |         run: go test . pkg/... -v | ||||||
|         env: |         env: | ||||||
| @@ -49,13 +47,13 @@ jobs: | |||||||
|     runs-on: macos-latest |     runs-on: macos-latest | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         go: [ '1.15', '1.14' ] |         go: [ '1.14', '1.13' ] | ||||||
|     name: Darwin Go ${{ matrix.go }} |     name: Darwin Go ${{ matrix.go }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2 | ||||||
|       - name: Setup Go |       - name: Setup Go | ||||||
|         uses: actions/setup-go@v2 |         uses: actions/setup-go@v1 | ||||||
|         with: |         with: | ||||||
|           go-version: ${{ matrix.go }} |           go-version: ${{ matrix.go }} | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
| @@ -66,15 +64,15 @@ jobs: | |||||||
|             libvpx \ |             libvpx \ | ||||||
|             x264 |             x264 | ||||||
|       - name: go vet |       - name: go vet | ||||||
|         run: go vet $(go list ./... | grep -v mmal) |         run: go vet ./... | ||||||
|       - name: go build |       - name: go build | ||||||
|         run: go build $(go list ./... | grep -v mmal) |         run: go build ./... | ||||||
|       - name: go build without CGO |       - name: go build without CGO | ||||||
|         run: go build . pkg/... |         run: go build . pkg/... | ||||||
|         env: |         env: | ||||||
|           CGO_ENABLED: 0 |           CGO_ENABLED: 0 | ||||||
|       - name: go test |       - name: go test | ||||||
|         run: go test -v -race $(go list ./... | grep -v mmal) |         run: go test ./... -v -race | ||||||
|       - name: go test without CGO |       - name: go test without CGO | ||||||
|         run: go test . pkg/... -v |         run: go test . pkg/... -v | ||||||
|         env: |         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"> | # mediadevices | ||||||
|   <br> |  | ||||||
|   Pion MediaDevices | Go implementation of the [MediaDevices](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices) API. | ||||||
|   <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> |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										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 | go 1.14 | ||||||
|  |  | ||||||
|  | replace github.com/pion/mediadevices => ../ | ||||||
|  |  | ||||||
| // Please don't commit require entries of examples. | // Please don't commit require entries of examples. | ||||||
| // `git checkout master examples/go.mod` to revert this file. | // `git checkout master examples/go.mod` to revert this file. | ||||||
| require github.com/pion/mediadevices v0.0.0 | require github.com/pion/mediadevices v0.0.0-00010101000000-000000000000 | ||||||
|  |  | ||||||
| replace github.com/pion/mediadevices v0.0.0 => ../ |  | ||||||
|   | |||||||
							
								
								
									
										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 | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"image/jpeg" | 	"image/jpeg" | ||||||
| 	"io" | 	"io" | ||||||
| @@ -21,12 +18,6 @@ import ( | |||||||
| 	_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter | 	_ "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() { | func main() { | ||||||
| 	s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ | 	s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{ | ||||||
| 		Video: func(constraint *mediadevices.MediaTrackConstraints) { | 		Video: func(constraint *mediadevices.MediaTrackConstraints) { | ||||||
| @@ -34,14 +25,16 @@ func main() { | |||||||
| 			constraint.Height = prop.Int(400) | 			constraint.Height = prop.Int(400) | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
| 	must(err) | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	t := s.GetVideoTracks()[0] | 	t := s.GetVideoTracks()[0] | ||||||
|  | 	defer t.Stop() | ||||||
| 	videoTrack := t.(*mediadevices.VideoTrack) | 	videoTrack := t.(*mediadevices.VideoTrack) | ||||||
| 
 | 
 | ||||||
| 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		var buf bytes.Buffer | 		videoReader := videoTrack.NewReader() | ||||||
| 		videoReader := videoTrack.NewReader(false) |  | ||||||
| 		mimeWriter := multipart.NewWriter(w) | 		mimeWriter := multipart.NewWriter(w) | ||||||
| 
 | 
 | ||||||
| 		contentType := fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary()) | 		contentType := fmt.Sprintf("multipart/x-mixed-replace;boundary=%s", mimeWriter.Boundary()) | ||||||
| @@ -51,24 +44,23 @@ func main() { | |||||||
| 		partHeader.Add("Content-Type", "image/jpeg") | 		partHeader.Add("Content-Type", "image/jpeg") | ||||||
| 
 | 
 | ||||||
| 		for { | 		for { | ||||||
| 			frame, release, err := videoReader.Read() | 			frame, err := videoReader.Read() | ||||||
| 			if err == io.EOF { | 			if err != nil { | ||||||
| 				return | 				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) | 			partWriter, err := mimeWriter.CreatePart(partHeader) | ||||||
| 			must(err) | 			if err != nil { | ||||||
|  | 				panic(err) | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
| 			_, err = partWriter.Write(buf.Bytes()) | 			err = jpeg.Encode(partWriter, frame, nil) | ||||||
| 			buf.Reset() | 			if err != nil { | ||||||
| 			must(err) | 				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 ( | require ( | ||||||
| 	github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 | 	github.com/blackjack/webcam v0.0.0-20200313125108-10ed912a8539 | ||||||
| 	github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93 | 	github.com/jfreymuth/pulse v0.0.0-20200804114219-7d61c4938214 | ||||||
| 	github.com/lherman-cs/opus v0.0.2 | 	github.com/lherman-cs/opus v0.0.0-20200223204610-6a4b98199ea4 | ||||||
| 	github.com/pion/logging v0.2.2 | 	github.com/pion/webrtc/v2 v2.2.23 | ||||||
| 	github.com/pion/webrtc/v2 v2.2.26 |  | ||||||
| 	github.com/satori/go.uuid v1.2.0 | 	github.com/satori/go.uuid v1.2.0 | ||||||
| 	golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 | 	golang.org/x/image v0.0.0-20200801110659-972c09e46d76 | ||||||
| 	golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f | 	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/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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= | ||||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | 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-20200804114219-7d61c4938214 h1:2xVJKIumEUWeV3vczQwn61SHjNZ94Bwk+4CTjmcePxk= | ||||||
| github.com/jfreymuth/pulse v0.0.0-20201014123913-1e525c426c93/go.mod h1:cpYspI6YljhkUf1WLXLLDmeaaPFc3CnGLjDZf9dZ4no= | 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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | 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/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | 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.0-20200223204610-6a4b98199ea4 h1:2ydMA2KbxRkYmIw3R8Me8dn90bejxBR4MKYXJ5THK3I= | ||||||
| github.com/lherman-cs/opus v0.0.2/go.mod h1:v9KQvlDYMuvlwniumBVMlrB0VHQvyTgxNvaXjPmTmps= | 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 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc= | ||||||
| github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= | 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= | 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/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 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= | ||||||
| github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | 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.19 h1:IcOmm5fdDzJVCMgFYDCMtFC+lrjG78KcMYXH+gOo6ys= | ||||||
| github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= | 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.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U= | ||||||
| github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E= | github.com/pion/dtls/v2 v2.0.2 h1:FHCHTiM182Y8e15aFTiORroiATUI16ryHiQh8AIOJ1E= | ||||||
| github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I= | 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/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 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk= | ||||||
| github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI= | 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.8 h1:tEWel2BKXLZitU+LxY3GDeQXoKeTafYasiu/X+XBKNM= | ||||||
| github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= | 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 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI= | ||||||
| github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= | github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= | ||||||
| github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw= | 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 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= | ||||||
| github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= | 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.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.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.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= | ||||||
| github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM= | github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM= | ||||||
| github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= | 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/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 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= | ||||||
| github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= | 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.23 h1:rZdOC95fwUCoQFVjHooPAayx/vhs3SLHFz8J/iRkAuk= | ||||||
| github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc= | 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 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/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/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.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.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.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 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= | ||||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | 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-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-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-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-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 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg= | ||||||
| golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | 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-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw= | ||||||
| golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | 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-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-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-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-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-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-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 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= | ||||||
| golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= | 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-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-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-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-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-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE= | ||||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | 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= | 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. | // MediaDeviceType definitions. | ||||||
| const ( | const ( | ||||||
| 	VideoInput MediaDeviceType = iota + 1 | 	VideoInput MediaDeviceType = iota | ||||||
| 	AudioInput | 	AudioInput | ||||||
| 	AudioOutput | 	AudioOutput | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ package mediadevices | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"math" | 	"math" | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/driver" | 	"github.com/pion/mediadevices/pkg/driver" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"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. | // 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 | // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia | ||||||
| func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) { | func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) { | ||||||
| 	trackers := make([]Track, 0) | 	tracks := make([]Track, 0) | ||||||
|  |  | ||||||
| 	cleanTrackers := func() { | 	cleanTracks := func() { | ||||||
| 		for _, t := range trackers { | 		for _, t := range tracks { | ||||||
| 			t.Close() | 			t.Stop() | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var videoConstraints MediaTrackConstraints |  | ||||||
| 	if constraints.Video != nil { | 	if constraints.Video != nil { | ||||||
| 		constraints.Video(&videoConstraints) | 		var p MediaTrackConstraints | ||||||
| 		tracker, err := selectScreen(videoConstraints, constraints.Codec) | 		constraints.Video(&p) | ||||||
|  | 		track, err := selectScreen(p) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			cleanTrackers() | 			cleanTracks() | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		trackers = append(trackers, tracker) | 		tracks = append(tracks, track) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s, err := NewMediaStream(trackers...) | 	s, err := NewMediaStream(tracks...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		cleanTrackers() | 		cleanTracks() | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -48,41 +47,41 @@ func GetDisplayMedia(constraints MediaStreamConstraints) (MediaStream, error) { | |||||||
| // with tracks containing the requested types of media. | // with tracks containing the requested types of media. | ||||||
| // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia | // Reference: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia | ||||||
| func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) { | func GetUserMedia(constraints MediaStreamConstraints) (MediaStream, error) { | ||||||
| 	// TODO: It should return media stream based on constraints | 	tracks := make([]Track, 0) | ||||||
| 	trackers := make([]Track, 0) |  | ||||||
|  |  | ||||||
| 	cleanTrackers := func() { | 	cleanTracks := func() { | ||||||
| 		for _, t := range trackers { | 		for _, t := range tracks { | ||||||
| 			t.Close() | 			t.Stop() | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var videoConstraints, audioConstraints MediaTrackConstraints |  | ||||||
| 	if constraints.Video != nil { | 	if constraints.Video != nil { | ||||||
| 		constraints.Video(&videoConstraints) | 		var p MediaTrackConstraints | ||||||
| 		tracker, err := selectVideo(videoConstraints, constraints.Codec) | 		constraints.Video(&p) | ||||||
|  | 		track, err := selectVideo(p) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			cleanTrackers() | 			cleanTracks() | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		trackers = append(trackers, tracker) | 		tracks = append(tracks, track) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if constraints.Audio != nil { | 	if constraints.Audio != nil { | ||||||
| 		constraints.Audio(&audioConstraints) | 		var p MediaTrackConstraints | ||||||
| 		tracker, err := selectAudio(audioConstraints, constraints.Codec) | 		constraints.Audio(&p) | ||||||
|  | 		track, err := selectAudio(p) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			cleanTrackers() | 			cleanTracks() | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		trackers = append(trackers, tracker) | 		tracks = append(tracks, track) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s, err := NewMediaStream(trackers...) | 	s, err := NewMediaStream(tracks...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		cleanTrackers() | 		cleanTracks() | ||||||
| 		return nil, err | 		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) { | func selectBestDriver(filter driver.FilterFn, constraints MediaTrackConstraints) (driver.Driver, MediaTrackConstraints, error) { | ||||||
| 	var bestDriver driver.Driver | 	var bestDriver driver.Driver | ||||||
| 	var bestProp prop.Media | 	var bestProp prop.Media | ||||||
| 	var foundPropertiesLog []string |  | ||||||
| 	minFitnessDist := math.Inf(1) | 	minFitnessDist := math.Inf(1) | ||||||
|  |  | ||||||
| 	foundPropertiesLog = append(foundPropertiesLog, "\n============ Found Properties ============") |  | ||||||
| 	driverProperties := queryDriverProperties(filter) | 	driverProperties := queryDriverProperties(filter) | ||||||
| 	for d, props := range driverProperties { | 	for d, props := range driverProperties { | ||||||
| 		priority := float64(d.Info().Priority) | 		priority := float64(d.Info().Priority) | ||||||
| 		for _, p := range props { | 		for _, p := range props { | ||||||
| 			foundPropertiesLog = append(foundPropertiesLog, p.String()) |  | ||||||
| 			fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p) | 			fitnessDist, ok := constraints.MediaConstraints.FitnessDistance(p) | ||||||
| 			if !ok { | 			if !ok { | ||||||
| 				continue | 				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 { | 	if bestDriver == nil { | ||||||
| 		foundPropertiesLog = append(foundPropertiesLog, "Not found") |  | ||||||
| 		logger.Debug(strings.Join(foundPropertiesLog, "\n\n")) |  | ||||||
| 		return nil, MediaTrackConstraints{}, errNotFound | 		return nil, MediaTrackConstraints{}, errNotFound | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	foundPropertiesLog = append(foundPropertiesLog, bestProp.String()) |  | ||||||
| 	logger.Debug(strings.Join(foundPropertiesLog, "\n\n")) |  | ||||||
| 	constraints.selectedMedia = prop.Media{} | 	constraints.selectedMedia = prop.Media{} | ||||||
| 	constraints.selectedMedia.MergeConstraints(constraints.MediaConstraints) | 	constraints.selectedMedia.MergeConstraints(constraints.MediaConstraints) | ||||||
| 	constraints.selectedMedia.Merge(bestProp) | 	constraints.selectedMedia.Merge(bestProp) | ||||||
| 	return bestDriver, constraints, nil | 	return bestDriver, constraints, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func selectAudio(constraints MediaTrackConstraints, selector *CodecSelector) (Track, error) { | func selectAudio(constraints MediaTrackConstraints) (Track, error) { | ||||||
| 	typeFilter := driver.FilterAudioRecorder() | 	typeFilter := driver.FilterAudioRecorder() | ||||||
|  |  | ||||||
| 	d, c, err := selectBestDriver(typeFilter, constraints) | 	d, c, err := selectBestDriver(typeFilter, constraints) | ||||||
| @@ -168,9 +156,10 @@ func selectAudio(constraints MediaTrackConstraints, selector *CodecSelector) (Tr | |||||||
| 		return nil, err | 		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() | 	typeFilter := driver.FilterVideoRecorder() | ||||||
| 	notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen)) | 	notScreenFilter := driver.FilterNot(driver.FilterDeviceType(driver.Screen)) | ||||||
| 	filter := driver.FilterAnd(typeFilter, notScreenFilter) | 	filter := driver.FilterAnd(typeFilter, notScreenFilter) | ||||||
| @@ -180,10 +169,10 @@ func selectVideo(constraints MediaTrackConstraints, selector *CodecSelector) (Tr | |||||||
| 		return nil, err | 		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() | 	typeFilter := driver.FilterVideoRecorder() | ||||||
| 	screenFilter := driver.FilterDeviceType(driver.Screen) | 	screenFilter := driver.FilterDeviceType(driver.Screen) | ||||||
| 	filter := driver.FilterAnd(typeFilter, screenFilter) | 	filter := driver.FilterAnd(typeFilter, screenFilter) | ||||||
| @@ -193,7 +182,7 @@ func selectScreen(constraints MediaTrackConstraints, selector *CodecSelector) (T | |||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return newTrackFromDriver(d, c, selector) | 	return newVideoTrack(d, c) | ||||||
| } | } | ||||||
|  |  | ||||||
| func EnumerateDevices() []MediaDeviceInfo { | 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 | package mediadevices | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"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" | ||||||
| 	_ "github.com/pion/mediadevices/pkg/driver/audiotest" | 	_ "github.com/pion/mediadevices/pkg/driver/audiotest" | ||||||
| 	_ "github.com/pion/mediadevices/pkg/driver/videotest" | 	_ "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" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestGetUserMedia(t *testing.T) { | 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{ | 	constraints := MediaStreamConstraints{ | ||||||
| 		Video: func(c *MediaTrackConstraints) { | 		Video: func(c *MediaTrackConstraints) { | ||||||
|  | 			c.Enabled = true | ||||||
| 			c.Width = prop.Int(640) | 			c.Width = prop.Int(640) | ||||||
| 			c.Height = prop.Int(480) | 			c.Height = prop.Int(480) | ||||||
|  | 			params := videoParams | ||||||
|  | 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{¶ms} | ||||||
| 		}, | 		}, | ||||||
| 		Audio: func(c *MediaTrackConstraints) { | 		Audio: func(c *MediaTrackConstraints) { | ||||||
|  | 			c.Enabled = true | ||||||
|  | 			params := audioParams | ||||||
|  | 			c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms} | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 	constraintsWrong := MediaStreamConstraints{ | 	constraintsWrong := MediaStreamConstraints{ | ||||||
| 		Video: func(c *MediaTrackConstraints) { | 		Video: func(c *MediaTrackConstraints) { | ||||||
| 			c.Width = prop.IntExact(10000) | 			c.Enabled = true | ||||||
|  | 			c.Width = prop.Int(640) | ||||||
| 			c.Height = prop.Int(480) | 			c.Height = prop.Int(480) | ||||||
|  | 			params := videoParams | ||||||
|  | 			params.BitRate = 0 | ||||||
|  | 			c.VideoEncoderBuilders = []codec.VideoEncoderBuilder{¶ms} | ||||||
| 		}, | 		}, | ||||||
| 		Audio: func(c *MediaTrackConstraints) { | 		Audio: func(c *MediaTrackConstraints) { | ||||||
|  | 			c.Enabled = true | ||||||
|  | 			params := audioParams | ||||||
|  | 			c.AudioEncoderBuilders = []codec.AudioEncoderBuilder{¶ms} | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | ======= | ||||||
|  | >>>>>>> ccd7985... Redesign GetUserMedia API | ||||||
|  |  | ||||||
| 	// GetUserMedia with broken parameters | 	// GetUserMedia with broken parameters | ||||||
| 	ms, err := GetUserMedia(constraintsWrong) | 	ms, err := md.GetUserMedia(constraints) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		t.Fatal("Expected error, but got 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 | 	// GetUserMedia with correct parameters | ||||||
| 	ms, err = GetUserMedia(constraints) | 	ms, err = md.GetUserMedia(constraints) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Unexpected error: %v", err) | 		t.Fatalf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -54,11 +135,11 @@ func TestGetUserMedia(t *testing.T) { | |||||||
| 	time.Sleep(50 * time.Millisecond) | 	time.Sleep(50 * time.Millisecond) | ||||||
|  |  | ||||||
| 	for _, track := range tracks { | 	for _, track := range tracks { | ||||||
| 		track.Close() | 		track.Stop() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Stop and retry GetUserMedia | 	// Stop and retry GetUserMedia | ||||||
| 	ms, err = GetUserMedia(constraints) | 	ms, err = md.GetUserMedia(constraints) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("Failed to GetUserMedia after the previsous tracks stopped: %v", err) | 		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) | 	time.Sleep(50 * time.Millisecond) | ||||||
| 	for _, track := range tracks { | 	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) { | func TestSelectBestDriverConstraintsResultIsSetProperly(t *testing.T) { | ||||||
| 	filterFn := driver.FilterVideoRecorder() | 	filterFn := driver.FilterVideoRecorder() | ||||||
| 	drivers := driver.GetManager().Query(filterFn) | 	drivers := driver.GetManager().Query(filterFn) | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ package mediadevices | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
|  | 	"github.com/pion/webrtc/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // MediaStream is an interface that represents a collection of existing tracks. | // MediaStream is an interface that represents a collection of existing tracks. | ||||||
| @@ -19,20 +21,21 @@ type MediaStream interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| type mediaStream struct { | type mediaStream struct { | ||||||
| 	tracks map[Track]struct{} | 	tracks map[string]Track | ||||||
| 	l      sync.RWMutex | 	l      sync.RWMutex | ||||||
| } | } | ||||||
|  |  | ||||||
| const trackTypeDefault MediaDeviceType = 0 | const rtpCodecTypeDefault webrtc.RTPCodecType = 0 | ||||||
|  |  | ||||||
| // NewMediaStream creates a MediaStream interface that's defined in | // NewMediaStream creates a MediaStream interface that's defined in | ||||||
| // https://w3c.github.io/mediacapture-main/#dom-mediastream | // https://w3c.github.io/mediacapture-main/#dom-mediastream | ||||||
| func NewMediaStream(tracks ...Track) (MediaStream, error) { | func NewMediaStream(tracks ...Track) (MediaStream, error) { | ||||||
| 	m := mediaStream{tracks: make(map[Track]struct{})} | 	m := mediaStream{tracks: make(map[string]Track)} | ||||||
|  |  | ||||||
| 	for _, track := range tracks { | 	for _, track := range tracks { | ||||||
| 		if _, ok := m.tracks[track]; !ok { | 		id := track.ID() | ||||||
| 			m.tracks[track] = struct{}{} | 		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 { | 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 { | 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 { | 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. | // 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. | // 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() | 	m.l.RLock() | ||||||
| 	defer m.l.RUnlock() | 	defer m.l.RUnlock() | ||||||
|  |  | ||||||
| 	result := make([]Track, 0) | 	result := make([]Track, 0) | ||||||
| 	for track := range m.tracks { | 	for _, track := range m.tracks { | ||||||
| 		if track.Kind() == t || t == trackTypeDefault { | 		if filter(track) { | ||||||
| 			result = append(result, track) | 			result = append(result, track) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -71,16 +74,17 @@ func (m *mediaStream) AddTrack(t Track) { | |||||||
| 	m.l.Lock() | 	m.l.Lock() | ||||||
| 	defer m.l.Unlock() | 	defer m.l.Unlock() | ||||||
|  |  | ||||||
| 	if _, ok := m.tracks[t]; ok { | 	id := t.ID() | ||||||
|  | 	if _, ok := m.tracks[id]; ok { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	m.tracks[t] = struct{}{} | 	m.tracks[id] = t | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *mediaStream) RemoveTrack(t Track) { | func (m *mediaStream) RemoveTrack(t Track) { | ||||||
| 	m.l.Lock() | 	m.l.Lock() | ||||||
| 	defer m.l.Unlock() | 	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 { | type MediaStreamConstraints struct { | ||||||
| 	Audio MediaOption | 	Audio MediaOption | ||||||
| 	Video MediaOption | 	Video MediaOption | ||||||
| 	Codec *CodecSelector |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // MediaTrackConstraints represents https://w3c.github.io/mediacapture-main/#dom-mediatrackconstraints | // 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: | // Read reads raw data, the format is determined by the media type and property: | ||||||
| //   - For video, each call will return a frame. | //   - For video, each call will return a frame. | ||||||
| //   - For audio, each call will return a chunk which its size configured by Latency | //   - 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 | 	data, ok := <-rc.dataChan | ||||||
| 	if !ok { | 	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 | // Close closes the capturing session, and no data will flow anymore | ||||||
|   | |||||||
| @@ -1,45 +1,21 @@ | |||||||
| package codec | package codec | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"io" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | 	"github.com/pion/mediadevices/pkg/io/audio" | ||||||
| 	"github.com/pion/mediadevices/pkg/io/video" | 	"github.com/pion/mediadevices/pkg/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"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 | // AudioEncoderBuilder is the interface that wraps basic operations that are | ||||||
| // necessary to build the audio encoder. | // necessary to build the audio encoder. | ||||||
| // | // | ||||||
| // This interface is for codec implementors to provide codec specific params, | // This interface is for codec implementors to provide codec specific params, | ||||||
| // but still giving generality for the users. | // but still giving generality for the users. | ||||||
| type AudioEncoderBuilder interface { | type AudioEncoderBuilder interface { | ||||||
| 	// RTPCodec represents the codec metadata | 	// Name represents the codec name | ||||||
| 	RTPCodec() *RTPCodec | 	Name() string | ||||||
| 	// BuildAudioEncoder builds audio encoder by given media params and audio input | 	// BuildAudioEncoder builds audio encoder by given media params and audio input | ||||||
| 	BuildAudioEncoder(r audio.Reader, p prop.Media) (ReadCloser, error) | 	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, | // This interface is for codec implementors to provide codec specific params, | ||||||
| // but still giving generality for the users. | // but still giving generality for the users. | ||||||
| type VideoEncoderBuilder interface { | type VideoEncoderBuilder interface { | ||||||
| 	// RTPCodec represents the codec metadata | 	// Name represents the codec name | ||||||
| 	RTPCodec() *RTPCodec | 	Name() string | ||||||
| 	// BuildVideoEncoder builds video encoder by given media params and video input | 	// BuildVideoEncoder builds video encoder by given media params and video input | ||||||
| 	BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error) | 	BuildVideoEncoder(r video.Reader, p prop.Media) (ReadCloser, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame | // ReadCloser is an io.ReadCloser with methods for rate limiting: SetBitRate and ForceKeyFrame | ||||||
| type ReadCloser interface { | type ReadCloser interface { | ||||||
| 	Read() (b []byte, release func(), err error) | 	io.ReadCloser | ||||||
| 	Close() error |  | ||||||
| 	// SetBitRate sets current target bitrate, lower bitrate means smaller data will be transmitted | 	// SetBitRate sets current target bitrate, lower bitrate means smaller data will be transmitted | ||||||
| 	// but this also means that the quality will also be lower. | 	// but this also means that the quality will also be lower. | ||||||
| 	SetBitRate(int) error | 	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" | 	"unsafe" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"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/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
| ) | ) | ||||||
| @@ -23,6 +24,7 @@ import ( | |||||||
| type encoder struct { | type encoder struct { | ||||||
| 	engine *C.Encoder | 	engine *C.Encoder | ||||||
| 	r      video.Reader | 	r      video.Reader | ||||||
|  | 	buff   []byte | ||||||
|  |  | ||||||
| 	mu     sync.Mutex | 	mu     sync.Mutex | ||||||
| 	closed bool | 	closed bool | ||||||
| @@ -50,17 +52,26 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoder) Read() ([]byte, func(), error) { | func (e *encoder) Read(p []byte) (n int, err error) { | ||||||
| 	e.mu.Lock() | 	e.mu.Lock() | ||||||
| 	defer e.mu.Unlock() | 	defer e.mu.Unlock() | ||||||
|  |  | ||||||
| 	if e.closed { | 	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 { | 	if err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	yuvImg := img.(*image.YCbCr) | 	yuvImg := img.(*image.YCbCr) | ||||||
| @@ -74,11 +85,16 @@ func (e *encoder) Read() ([]byte, func(), error) { | |||||||
| 		width:  C.int(bounds.Max.X - bounds.Min.X), | 		width:  C.int(bounds.Max.X - bounds.Min.X), | ||||||
| 	}, &rv) | 	}, &rv) | ||||||
| 	if err := errResult(rv); err != nil { | 	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) | 	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 { | func (e *encoder) SetBitRate(b int) error { | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"github.com/pion/mediadevices/pkg/codec" | ||||||
| 	"github.com/pion/mediadevices/pkg/io/video" | 	"github.com/pion/mediadevices/pkg/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
|  | 	"github.com/pion/webrtc/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Params stores libopenh264 specific encoding parameters. | // Params stores libopenh264 specific encoding parameters. | ||||||
| @@ -20,9 +21,9 @@ func NewParams() (Params, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | func (p *Params) Name() string { | ||||||
| 	return codec.NewRTPH264Codec(90000) | 	return webrtc.H264 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildVideoEncoder builds openh264 encoder with given params | // 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 | 	return &e, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoder) Read() ([]byte, func(), error) { | func (e *encoder) Read(p []byte) (int, error) { | ||||||
| 	buff, _, err := e.reader.Read() | 	buff, err := e.reader.Read() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	encoded := make([]byte, 1024) |  | ||||||
| 	switch b := buff.(type) { | 	switch b := buff.(type) { | ||||||
| 	case *wave.Int16Interleaved: | 	case *wave.Int16Interleaved: | ||||||
| 		n, err := e.engine.Encode(b.Data, encoded) | 		n, err := e.engine.Encode(b.Data, p) | ||||||
| 		return encoded[:n:n], func() {}, err | 		if err != nil { | ||||||
|  | 			return n, err | ||||||
|  | 		} | ||||||
|  | 		return n, nil | ||||||
| 	case *wave.Float32Interleaved: | 	case *wave.Float32Interleaved: | ||||||
| 		n, err := e.engine.EncodeFloat32(b.Data, encoded) | 		n, err := e.engine.EncodeFloat32(b.Data, p) | ||||||
| 		return encoded[:n:n], func() {}, err | 		if err != nil { | ||||||
|  | 			return n, err | ||||||
|  | 		} | ||||||
|  | 		return n, nil | ||||||
| 	default: | 	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/io/audio" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
| 	"github.com/pion/mediadevices/pkg/wave/mixer" | 	"github.com/pion/mediadevices/pkg/wave/mixer" | ||||||
|  | 	"github.com/pion/webrtc/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Params stores opus specific encoding parameters. | // Params stores opus specific encoding parameters. | ||||||
| @@ -19,9 +20,9 @@ func NewParams() (Params, error) { | |||||||
| 	return Params{}, nil | 	return Params{}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | func (p *Params) Name() string { | ||||||
| 	return codec.NewRTPOpusCodec(48000) | 	return webrtc.Opus | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildAudioEncoder builds opus encoder with given params | // BuildAudioEncoder builds opus encoder with given params | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"github.com/pion/mediadevices/pkg/codec" | ||||||
| 	"github.com/pion/mediadevices/pkg/io/video" | 	"github.com/pion/mediadevices/pkg/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
|  | 	"github.com/pion/webrtc/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ParamsVP8 stores VP8 encoding parameters. | // ParamsVP8 stores VP8 encoding parameters. | ||||||
| @@ -43,9 +44,9 @@ func NewVP8Params() (ParamsVP8, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *ParamsVP8) RTPCodec() *codec.RTPCodec { | func (p *ParamsVP8) Name() string { | ||||||
| 	return codec.NewRTPVP8Codec(90000) | 	return webrtc.VP8 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildVideoEncoder builds VP8 encoder with given params | // BuildVideoEncoder builds VP8 encoder with given params | ||||||
| @@ -112,9 +113,9 @@ func NewVP9Params() (ParamsVP9, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *ParamsVP9) RTPCodec() *codec.RTPCodec { | func (p *ParamsVP9) Name() string { | ||||||
| 	return codec.NewRTPVP9Codec(90000) | 	return webrtc.VP9 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildVideoEncoder builds VP9 encoder with given params | // BuildVideoEncoder builds VP9 encoder with given params | ||||||
|   | |||||||
| @@ -64,6 +64,7 @@ import ( | |||||||
| 	"unsafe" | 	"unsafe" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"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/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
| ) | ) | ||||||
| @@ -79,6 +80,7 @@ const ( | |||||||
|  |  | ||||||
| type encoderVP8 struct { | type encoderVP8 struct { | ||||||
| 	r     video.Reader | 	r     video.Reader | ||||||
|  | 	buf   []byte | ||||||
| 	frame []byte | 	frame []byte | ||||||
|  |  | ||||||
| 	fdDRI    C.int | 	fdDRI    C.int | ||||||
| @@ -295,17 +297,25 @@ func newVP8Encoder(r video.Reader, p prop.Media, params ParamsVP8) (codec.ReadCl | |||||||
| 	return e, nil | 	return e, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoderVP8) Read() ([]byte, func(), error) { | func (e *encoderVP8) Read(p []byte) (int, error) { | ||||||
| 	e.mu.Lock() | 	e.mu.Lock() | ||||||
| 	defer e.mu.Unlock() | 	defer e.mu.Unlock() | ||||||
|  |  | ||||||
| 	if e.closed { | 	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 { | 	if err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| 	yuvImg := img.(*image.YCbCr) | 	yuvImg := img.(*image.YCbCr) | ||||||
|  |  | ||||||
| @@ -347,7 +357,7 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if e.picParam.reconstructed_frame == C.VA_INVALID_SURFACE { | 		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) | 		C.setForceKFFlagVP8(&e.picParam, 0) | ||||||
| @@ -415,7 +425,7 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | |||||||
| 			C.size_t(uintptr(p.src)), | 			C.size_t(uintptr(p.src)), | ||||||
| 			&id, | 			&id, | ||||||
| 		); s != C.VA_STATUS_SUCCESS { | 		); 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) | 		buffs = append(buffs, id) | ||||||
| 	} | 	} | ||||||
| @@ -425,17 +435,17 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | |||||||
| 		e.display, e.ctxID, | 		e.display, e.ctxID, | ||||||
| 		e.surfs[surfaceVP8Input], | 		e.surfs[surfaceVP8Input], | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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 | 	// Upload image | ||||||
| 	var vaImg C.VAImage | 	var vaImg C.VAImage | ||||||
| 	var rawBuf unsafe.Pointer | 	var rawBuf unsafe.Pointer | ||||||
| 	if s := C.vaDeriveImage(e.display, e.surfs[surfaceVP8Input], &vaImg); s != C.VA_STATUS_SUCCESS { | 	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 { | 	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 | 	// TODO: use vaImg.pitches to support padding | ||||||
| 	C.memcpy( | 	C.memcpy( | ||||||
| @@ -451,10 +461,10 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | |||||||
| 		unsafe.Pointer(&yuvImg.Cr[0]), C.size_t(len(yuvImg.Cr)), | 		unsafe.Pointer(&yuvImg.Cr[0]), C.size_t(len(yuvImg.Cr)), | ||||||
| 	) | 	) | ||||||
| 	if s := C.vaUnmapBuffer(e.display, vaImg.buf); s != C.VA_STATUS_SUCCESS { | 	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 { | 	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( | 	if s := C.vaRenderPicture( | ||||||
| @@ -462,38 +472,38 @@ func (e *encoderVP8) Read() ([]byte, func(), error) { | |||||||
| 		&buffs[1], // 0 is for ouput | 		&buffs[1], // 0 is for ouput | ||||||
| 		C.int(len(buffs)-1), | 		C.int(len(buffs)-1), | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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( | 	if s := C.vaEndPicture( | ||||||
| 		e.display, e.ctxID, | 		e.display, e.ctxID, | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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 | 	// Load encoded data | ||||||
| 	for retry := 3; retry >= 0; retry-- { | 	for retry := 3; retry >= 0; retry-- { | ||||||
| 		if s := C.vaSyncSurface(e.display, e.picParam.reconstructed_frame); s != C.VA_STATUS_SUCCESS { | 		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 | 		var surfStat C.VASurfaceStatus | ||||||
| 		if s := C.vaQuerySurfaceStatus( | 		if s := C.vaQuerySurfaceStatus( | ||||||
| 			e.display, e.picParam.reconstructed_frame, &surfStat, | 			e.display, e.picParam.reconstructed_frame, &surfStat, | ||||||
| 		); s != C.VA_STATUS_SUCCESS { | 		); 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 { | 		if surfStat == C.VASurfaceReady { | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 		if retry == 0 { | 		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 | 	var seg *C.VACodedBufferSegment | ||||||
| 	if s := C.vaMapBufferSeg(e.display, buffs[0], &seg); s != C.VA_STATUS_SUCCESS { | 	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 { | 	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) { | 	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 { | 	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 | 	// Destroy buffers | ||||||
| 	for _, b := range buffs { | 	for _, b := range buffs { | ||||||
| 		if s := C.vaDestroyBuffer(e.display, b); s != C.VA_STATUS_SUCCESS { | 		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 | 	e.picParam.ref_last_frame = e.picParam.reconstructed_frame | ||||||
| 	C.setRefreshLastFlagVP8(&e.picParam, 1) | 	C.setRefreshLastFlagVP8(&e.picParam, 1) | ||||||
|  |  | ||||||
| 	encoded := make([]byte, len(e.frame)) | 	n, err := mio.Copy(p, e.frame) | ||||||
| 	copy(encoded, e.frame) | 	if err != nil { | ||||||
| 	return encoded, func() {}, err | 		e.buf = e.frame | ||||||
|  | 	} | ||||||
|  | 	return n, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoderVP8) SetBitRate(b int) error { | func (e *encoderVP8) SetBitRate(b int) error { | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ import ( | |||||||
| 	"unsafe" | 	"unsafe" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"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/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
| ) | ) | ||||||
| @@ -66,6 +67,7 @@ const ( | |||||||
|  |  | ||||||
| type encoderVP9 struct { | type encoderVP9 struct { | ||||||
| 	r     video.Reader | 	r     video.Reader | ||||||
|  | 	buf   []byte | ||||||
| 	frame []byte | 	frame []byte | ||||||
|  |  | ||||||
| 	fdDRI    C.int | 	fdDRI    C.int | ||||||
| @@ -284,17 +286,25 @@ func newVP9Encoder(r video.Reader, p prop.Media, params ParamsVP9) (codec.ReadCl | |||||||
| 	return e, nil | 	return e, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoderVP9) Read() ([]byte, func(), error) { | func (e *encoderVP9) Read(p []byte) (int, error) { | ||||||
| 	e.mu.Lock() | 	e.mu.Lock() | ||||||
| 	defer e.mu.Unlock() | 	defer e.mu.Unlock() | ||||||
|  |  | ||||||
| 	if e.closed { | 	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 { | 	if err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| 	yuvImg := img.(*image.YCbCr) | 	yuvImg := img.(*image.YCbCr) | ||||||
|  |  | ||||||
| @@ -378,7 +388,7 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | |||||||
| 			C.size_t(uintptr(p.src)), | 			C.size_t(uintptr(p.src)), | ||||||
| 			&id, | 			&id, | ||||||
| 		); s != C.VA_STATUS_SUCCESS { | 		); 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) | 		buffs = append(buffs, id) | ||||||
| 	} | 	} | ||||||
| @@ -388,17 +398,17 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | |||||||
| 		e.display, e.ctxID, | 		e.display, e.ctxID, | ||||||
| 		e.surfs[surfaceVP9Input], | 		e.surfs[surfaceVP9Input], | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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 | 	// Upload image | ||||||
| 	var vaImg C.VAImage | 	var vaImg C.VAImage | ||||||
| 	var rawBuf unsafe.Pointer | 	var rawBuf unsafe.Pointer | ||||||
| 	if s := C.vaDeriveImage(e.display, e.surfs[surfaceVP9Input], &vaImg); s != C.VA_STATUS_SUCCESS { | 	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 { | 	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 | 	// TODO: use vaImg.pitches to support padding | ||||||
| 	C.copyI420toNV12( | 	C.copyI420toNV12( | ||||||
| @@ -409,10 +419,10 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | |||||||
| 		C.uint(len(yuvImg.Y)), | 		C.uint(len(yuvImg.Y)), | ||||||
| 	) | 	) | ||||||
| 	if s := C.vaUnmapBuffer(e.display, vaImg.buf); s != C.VA_STATUS_SUCCESS { | 	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 { | 	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( | 	if s := C.vaRenderPicture( | ||||||
| @@ -420,27 +430,27 @@ func (e *encoderVP9) Read() ([]byte, func(), error) { | |||||||
| 		&buffs[1], // 0 is for ouput | 		&buffs[1], // 0 is for ouput | ||||||
| 		C.int(len(buffs)-1), | 		C.int(len(buffs)-1), | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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( | 	if s := C.vaEndPicture( | ||||||
| 		e.display, e.ctxID, | 		e.display, e.ctxID, | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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 | 	// Load encoded data | ||||||
| 	if s := C.vaSyncSurface(e.display, e.picParam.reconstructed_frame); s != C.VA_STATUS_SUCCESS { | 	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 | 	var surfStat C.VASurfaceStatus | ||||||
| 	if s := C.vaQuerySurfaceStatus( | 	if s := C.vaQuerySurfaceStatus( | ||||||
| 		e.display, e.picParam.reconstructed_frame, &surfStat, | 		e.display, e.picParam.reconstructed_frame, &surfStat, | ||||||
| 	); s != C.VA_STATUS_SUCCESS { | 	); 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 | 	var seg *C.VACodedBufferSegment | ||||||
| 	if s := C.vaMapBufferSeg(e.display, buffs[0], &seg); s != C.VA_STATUS_SUCCESS { | 	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) { | 	if cap(e.frame) < int(seg.size) { | ||||||
| 		e.frame = make([]byte, 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 { | 	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 | 	// Destroy buffers | ||||||
| 	for _, b := range buffs { | 	for _, b := range buffs { | ||||||
| 		if s := C.vaDestroyBuffer(e.display, b); s != C.VA_STATUS_SUCCESS { | 		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 | 		e.slotCurr = 0 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	encoded := make([]byte, len(e.frame)) | 	n, err := mio.Copy(p, e.frame) | ||||||
| 	copy(encoded, e.frame) | 	if err != nil { | ||||||
| 	return encoded, func() {}, err | 		e.buf = e.frame | ||||||
|  | 	} | ||||||
|  | 	return n, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoderVP9) SetBitRate(b int) error { | func (e *encoderVP9) SetBitRate(b int) error { | ||||||
|   | |||||||
| @@ -56,8 +56,10 @@ import ( | |||||||
| 	"unsafe" | 	"unsafe" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"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/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
|  | 	"github.com/pion/webrtc/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type encoder struct { | type encoder struct { | ||||||
| @@ -66,6 +68,7 @@ type encoder struct { | |||||||
| 	cfg        *C.vpx_codec_enc_cfg_t | 	cfg        *C.vpx_codec_enc_cfg_t | ||||||
| 	r          video.Reader | 	r          video.Reader | ||||||
| 	frameIndex int | 	frameIndex int | ||||||
|  | 	buff       []byte | ||||||
| 	tStart     int | 	tStart     int | ||||||
| 	tLastFrame int | 	tLastFrame int | ||||||
| 	frame      []byte | 	frame      []byte | ||||||
| @@ -92,9 +95,9 @@ func NewVP8Params() (VP8Params, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *VP8Params) RTPCodec() *codec.RTPCodec { | func (p *VP8Params) Name() string { | ||||||
| 	return codec.NewRTPVP8Codec(90000) | 	return webrtc.VP8 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildVideoEncoder builds VP8 encoder with given params | // BuildVideoEncoder builds VP8 encoder with given params | ||||||
| @@ -119,9 +122,9 @@ func NewVP9Params() (VP9Params, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *VP9Params) RTPCodec() *codec.RTPCodec { | func (p *VP9Params) Name() string { | ||||||
| 	return codec.NewRTPVP9Codec(90000) | 	return webrtc.VP9 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildVideoEncoder builds VP9 encoder with given params | // 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 | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoder) Read() ([]byte, func(), error) { | func (e *encoder) Read(p []byte) (int, error) { | ||||||
| 	e.mu.Lock() | 	e.mu.Lock() | ||||||
| 	defer e.mu.Unlock() | 	defer e.mu.Unlock() | ||||||
|  |  | ||||||
| 	if e.closed { | 	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 { | 	if err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| 	yuvImg := img.(*image.YCbCr) | 	yuvImg := img.(*image.YCbCr) | ||||||
| 	bounds := yuvImg.Bounds() | 	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) { | 	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) | 		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 { | 		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.w, e.raw.h = C.uint(width), C.uint(height) | ||||||
| 		e.raw.r_w, e.raw.r_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.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]), | 		(*C.uchar)(&yuvImg.Y[0]), (*C.uchar)(&yuvImg.Cb[0]), (*C.uchar)(&yuvImg.Cr[0]), | ||||||
| 	); ec != C.VPX_CODEC_OK { | 	); ec != C.VPX_CODEC_OK { | ||||||
| 		return nil, func() {}, fmt.Errorf("vpx_codec_encode failed (%d)", ec) | 		return 0, fmt.Errorf("vpx_codec_encode failed (%d)", ec) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e.frameIndex++ | 	e.frameIndex++ | ||||||
| @@ -261,10 +272,11 @@ func (e *encoder) Read() ([]byte, func(), error) { | |||||||
| 			e.frame = append(e.frame, encoded...) | 			e.frame = append(e.frame, encoded...) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	n, err := mio.Copy(p, e.frame) | ||||||
| 	encoded := make([]byte, len(e.frame)) | 	if err != nil { | ||||||
| 	copy(encoded, e.frame) | 		e.buff = e.frame | ||||||
| 	return encoded, func() {}, err | 	} | ||||||
|  | 	return n, err | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoder) SetBitRate(b int) error { | 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_repeat_headers = 1; | ||||||
|   e->param.b_annexb = 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; |     *rc = ERR_APPLY_PROFILE; | ||||||
|     goto fail; |     goto fail; | ||||||
|   } |   } | ||||||
| @@ -95,4 +95,4 @@ void enc_close(Encoder *e, int *rc) { | |||||||
|   x264_encoder_close(e->h); |   x264_encoder_close(e->h); | ||||||
|   x264_picture_clean(&e->pic_in); |   x264_picture_clean(&e->pic_in); | ||||||
|   free(e); |   free(e); | ||||||
| } | } | ||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"github.com/pion/mediadevices/pkg/codec" | ||||||
| 	"github.com/pion/mediadevices/pkg/io/video" | 	"github.com/pion/mediadevices/pkg/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
|  | 	"github.com/pion/webrtc/v2" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Params stores libx264 specific encoding parameters. | // Params stores libx264 specific encoding parameters. | ||||||
| @@ -39,9 +40,9 @@ func NewParams() (Params, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // RTPCodec represents the codec metadata | // Name represents the codec name | ||||||
| func (p *Params) RTPCodec() *codec.RTPCodec { | func (p *Params) Name() string { | ||||||
| 	return codec.NewRTPH264Codec(90000) | 	return webrtc.H264 | ||||||
| } | } | ||||||
|  |  | ||||||
| // BuildVideoEncoder builds x264 encoder with given params | // BuildVideoEncoder builds x264 encoder with given params | ||||||
|   | |||||||
| @@ -14,12 +14,14 @@ import ( | |||||||
| 	"unsafe" | 	"unsafe" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/codec" | 	"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/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/prop" | 	"github.com/pion/mediadevices/pkg/prop" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type encoder struct { | type encoder struct { | ||||||
| 	engine *C.Encoder | 	engine *C.Encoder | ||||||
|  | 	buff   []byte | ||||||
| 	r      video.Reader | 	r      video.Reader | ||||||
| 	mu     sync.Mutex | 	mu     sync.Mutex | ||||||
| 	closed bool | 	closed bool | ||||||
| @@ -94,17 +96,25 @@ func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, | |||||||
| 	return &e, nil | 	return &e, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e *encoder) Read() ([]byte, func(), error) { | func (e *encoder) Read(p []byte) (int, error) { | ||||||
| 	e.mu.Lock() | 	e.mu.Lock() | ||||||
| 	defer e.mu.Unlock() | 	defer e.mu.Unlock() | ||||||
|  |  | ||||||
| 	if e.closed { | 	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 { | 	if err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| 	yuvImg := img.(*image.YCbCr) | 	yuvImg := img.(*image.YCbCr) | ||||||
|  |  | ||||||
| @@ -117,11 +127,15 @@ func (e *encoder) Read() ([]byte, func(), error) { | |||||||
| 		&rc, | 		&rc, | ||||||
| 	) | 	) | ||||||
| 	if err := errFromC(rc); err != nil { | 	if err := errFromC(rc); err != nil { | ||||||
| 		return nil, func() {}, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	encoded := C.GoBytes(unsafe.Pointer(s.data), s.data_len) | 	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 { | func (e *encoder) SetBitRate(b int) error { | ||||||
|   | |||||||
| @@ -52,10 +52,10 @@ func (d *dummy) AudioRecord(p prop.Media) (audio.Reader, error) { | |||||||
|  |  | ||||||
| 	closed := d.closed | 	closed := d.closed | ||||||
|  |  | ||||||
| 	reader := audio.ReaderFunc(func() (wave.Audio, func(), error) { | 	reader := audio.ReaderFunc(func() (wave.Audio, error) { | ||||||
| 		select { | 		select { | ||||||
| 		case <-closed: | 		case <-closed: | ||||||
| 			return nil, func() {}, io.EOF | 			return nil, io.EOF | ||||||
| 		default: | 		default: | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -78,7 +78,7 @@ func (d *dummy) AudioRecord(p prop.Media) (audio.Reader, error) { | |||||||
| 				a.SetFloat32(i, ch, wave.Float32Sample(sin[phase])) | 				a.SetFloat32(i, ch, wave.Float32Sample(sin[phase])) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return a, func() {}, nil | 		return a, nil | ||||||
| 	}) | 	}) | ||||||
| 	return reader, nil | 	return reader, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -56,10 +56,10 @@ func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | 	r := video.ReaderFunc(func() (image.Image, error) { | ||||||
| 		frame, _, err := rc.Read() | 		frame, err := rc.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, func() {}, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		return decoder.Decode(frame, property.Width, property.Height) | 		return decoder.Decode(frame, property.Width, property.Height) | ||||||
| 	}) | 	}) | ||||||
|   | |||||||
| @@ -8,8 +8,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"image" | 	"image" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"io/ioutil" | ||||||
| 	"path/filepath" |  | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/blackjack/webcam" | 	"github.com/blackjack/webcam" | ||||||
| @@ -26,36 +25,6 @@ const ( | |||||||
| var ( | var ( | ||||||
| 	errReadTimeout = errors.New("read timeout") | 	errReadTimeout = errors.New("read timeout") | ||||||
| 	errEmptyFrame  = errors.New("empty frame") | 	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 | // Camera implementation using v4l2 | ||||||
| @@ -71,47 +40,27 @@ type camera struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	discovered := make(map[string]struct{}) | 	searchPath := "/dev/v4l/by-path/" | ||||||
|  | 	devices, err := ioutil.ReadDir(searchPath) | ||||||
| 	discover := func(pattern string) { | 	if err != nil { | ||||||
| 		devices, err := filepath.Glob(pattern) | 		// No v4l device. | ||||||
| 		if err != nil { | 		return | ||||||
| 			// No v4l device. | 	} | ||||||
| 			return | 	for _, device := range devices { | ||||||
| 		} | 		cam := newCamera(searchPath + device.Name()) | ||||||
| 		for _, device := range devices { | 		driver.GetManager().Register(cam, driver.Info{ | ||||||
| 			label := filepath.Base(device) | 			Label:      device.Name(), | ||||||
| 			reallink, err := os.Readlink(device) | 			DeviceType: driver.Camera, | ||||||
| 			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, |  | ||||||
| 			}) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	discover("/dev/v4l/by-path/*") |  | ||||||
| 	discover("/dev/video*") |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func newCamera(path string) *camera { | func newCamera(path string) *camera { | ||||||
| 	formats := map[webcam.PixelFormat]frame.Format{ | 	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_YUYV):   frame.FormatYUYV, | 		webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY):  frame.FormatUYVY, | ||||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_UYVY):   frame.FormatUYVY, | 		webcam.PixelFormat(C.V4L2_PIX_FMT_NV12):  frame.FormatNV21, | ||||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_NV12):   frame.FormatNV21, | 		webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG): frame.FormatMJPEG, | ||||||
| 		webcam.PixelFormat(C.V4L2_PIX_FMT_MJPEG):  frame.FormatMJPEG, |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	reversedFormats := make(map[frame.Format]webcam.PixelFormat) | 	reversedFormats := make(map[frame.Format]webcam.PixelFormat) | ||||||
| @@ -133,8 +82,6 @@ func (c *camera) Open() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Late frames should be discarded. Buffering should be handled in higher level. |  | ||||||
| 	cam.SetBufferCount(1) |  | ||||||
| 	c.cam = cam | 	c.cam = cam | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @@ -182,7 +129,7 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | |||||||
| 	ctx, cancel := context.WithCancel(context.Background()) | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
| 	c.cancel = cancel | 	c.cancel = cancel | ||||||
| 	var buf []byte | 	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() | 		// Lock to avoid accessing the buffer after StopStreaming() | ||||||
| 		c.mutex.Lock() | 		c.mutex.Lock() | ||||||
| 		defer c.mutex.Unlock() | 		defer c.mutex.Unlock() | ||||||
| @@ -191,23 +138,23 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | |||||||
| 		for i := 0; i < maxEmptyFrameCount; i++ { | 		for i := 0; i < maxEmptyFrameCount; i++ { | ||||||
| 			if ctx.Err() != nil { | 			if ctx.Err() != nil { | ||||||
| 				// Return EOF if the camera is already closed. | 				// Return EOF if the camera is already closed. | ||||||
| 				return nil, func() {}, io.EOF | 				return nil, io.EOF | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			err := cam.WaitForFrame(5) // 5 seconds | 			err := cam.WaitForFrame(5) // 5 seconds | ||||||
| 			switch err.(type) { | 			switch err.(type) { | ||||||
| 			case nil: | 			case nil: | ||||||
| 			case *webcam.Timeout: | 			case *webcam.Timeout: | ||||||
| 				return nil, func() {}, errReadTimeout | 				return nil, errReadTimeout | ||||||
| 			default: | 			default: | ||||||
| 				// Camera has been stopped. | 				// Camera has been stopped. | ||||||
| 				return nil, func() {}, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			b, err := cam.ReadFrame() | 			b, err := cam.ReadFrame() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				// Camera has been stopped. | 				// Camera has been stopped. | ||||||
| 				return nil, func() {}, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Frame is empty. | 			// Frame is empty. | ||||||
| @@ -227,7 +174,7 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | |||||||
| 			n := copy(buf, b) | 			n := copy(buf, b) | ||||||
| 			return decoder.Decode(buf[:n], p.Width, p.Height) | 			return decoder.Decode(buf[:n], p.Width, p.Height) | ||||||
| 		} | 		} | ||||||
| 		return nil, func() {}, errEmptyFrame | 		return nil, errEmptyFrame | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	return r, nil | 	return r, nil | ||||||
| @@ -237,46 +184,13 @@ func (c *camera) Properties() []prop.Media { | |||||||
| 	properties := make([]prop.Media, 0) | 	properties := make([]prop.Media, 0) | ||||||
| 	for format := range c.cam.GetSupportedFormats() { | 	for format := range c.cam.GetSupportedFormats() { | ||||||
| 		for _, frameSize := range c.cam.GetSupportedFrameSizes(format) { | 		for _, frameSize := range c.cam.GetSupportedFrameSizes(format) { | ||||||
| 			supportedFormat, ok := c.formats[format] | 			properties = append(properties, prop.Media{ | ||||||
| 			if !ok { | 				Video: prop.Video{ | ||||||
| 				continue | 					Width:       int(frameSize.MaxWidth), | ||||||
| 			} | 					Height:      int(frameSize.MaxHeight), | ||||||
|  | 					FrameFormat: c.formats[format], | ||||||
| 			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, |  | ||||||
| 						}, |  | ||||||
| 					}) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return properties | 	return properties | ||||||
|   | |||||||
| @@ -116,10 +116,10 @@ func (c *camera) VideoRecord(p prop.Media) (video.Reader, error) { | |||||||
|  |  | ||||||
| 	img := &image.YCbCr{} | 	img := &image.YCbCr{} | ||||||
|  |  | ||||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | 	r := video.ReaderFunc(func() (image.Image, error) { | ||||||
| 		b, ok := <-c.ch | 		b, ok := <-c.ch | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			return nil, func() {}, io.EOF | 			return nil, io.EOF | ||||||
| 		} | 		} | ||||||
| 		img.Y = b[:nPix] | 		img.Y = b[:nPix] | ||||||
| 		img.Cb = b[nPix : nPix+nPix/2] | 		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.CStride = p.Width / 2 | ||||||
| 		img.SubsampleRatio = image.YCbCrSubsampleRatio422 | 		img.SubsampleRatio = image.YCbCrSubsampleRatio422 | ||||||
| 		img.Rect = image.Rect(0, 0, p.Width, p.Height) | 		img.Rect = image.Rect(0, 0, p.Width, p.Height) | ||||||
| 		return img, func() {}, nil | 		return img, nil | ||||||
| 	}) | 	}) | ||||||
| 	return r, nil | 	return r, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,204 +1 @@ | |||||||
| package microphone | 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 | 	var dst image.RGBA | ||||||
| 	reader := s.reader | 	reader := s.reader | ||||||
|  |  | ||||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | 	r := video.ReaderFunc(func() (image.Image, error) { | ||||||
| 		<-s.tick.C | 		<-s.tick.C | ||||||
| 		return reader.Read().ToRGBA(&dst), func() {}, nil | 		return reader.Read().ToRGBA(&dst), nil | ||||||
| 	}) | 	}) | ||||||
| 	return r, nil | 	return r, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -103,10 +103,10 @@ func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) { | |||||||
| 	d.tick = tick | 	d.tick = tick | ||||||
| 	closed := d.closed | 	closed := d.closed | ||||||
|  |  | ||||||
| 	r := video.ReaderFunc(func() (image.Image, func(), error) { | 	r := video.ReaderFunc(func() (image.Image, error) { | ||||||
| 		select { | 		select { | ||||||
| 		case <-closed: | 		case <-closed: | ||||||
| 			return nil, func() {}, io.EOF | 			return nil, io.EOF | ||||||
| 		default: | 		default: | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -130,7 +130,7 @@ func (d *dummy) VideoRecord(p prop.Media) (video.Reader, error) { | |||||||
| 			CStride:        p.Width / 2, | 			CStride:        p.Width / 2, | ||||||
| 			SubsampleRatio: image.YCbCrSubsampleRatio422, | 			SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||||
| 			Rect:           image.Rect(0, 0, p.Width, p.Height), | 			Rect:           image.Rect(0, 0, p.Width, p.Height), | ||||||
| 		}, func() {}, nil | 		}, nil | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	return r, nil | 	return r, nil | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ import ( | |||||||
| 	"image/jpeg" | 	"image/jpeg" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func decodeMJPEG(frame []byte, width, height int) (image.Image, func(), error) { | func decodeMJPEG(frame []byte, width, height int) (image.Image, error) { | ||||||
| 	img, err := jpeg.Decode(bytes.NewReader(frame)) | 	return jpeg.Decode(bytes.NewReader(frame)) | ||||||
| 	return img, func() {}, err |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ package frame | |||||||
| type Format string | type Format string | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|  | 	// YUV Formats | ||||||
|  |  | ||||||
| 	// FormatI420 https://www.fourcc.org/pixel-format/yuv-i420/ | 	// FormatI420 https://www.fourcc.org/pixel-format/yuv-i420/ | ||||||
| 	FormatI420 Format = "I420" | 	FormatI420 Format = "I420" | ||||||
| 	// FormatI444 is a YUV format without sub-sampling | 	// FormatI444 is a YUV format without sub-sampling | ||||||
| @@ -14,11 +16,18 @@ const ( | |||||||
| 	// FormatUYVY https://www.fourcc.org/pixel-format/yuv-uyvy/ | 	// FormatUYVY https://www.fourcc.org/pixel-format/yuv-uyvy/ | ||||||
| 	FormatUYVY = "UYVY" | 	FormatUYVY = "UYVY" | ||||||
|  |  | ||||||
|  | 	// RGB Formats | ||||||
|  |  | ||||||
| 	// FormatRGBA https://www.fourcc.org/pixel-format/rgb-rgba/ | 	// FormatRGBA https://www.fourcc.org/pixel-format/rgb-rgba/ | ||||||
| 	FormatRGBA Format = "RGBA" | 	FormatRGBA Format = "RGBA" | ||||||
|  |  | ||||||
|  | 	// Compressed Formats | ||||||
|  |  | ||||||
| 	// FormatMJPEG https://www.fourcc.org/mjpg/ | 	// FormatMJPEG https://www.fourcc.org/mjpg/ | ||||||
| 	FormatMJPEG = "MJPEG" | 	FormatMJPEG = "MJPEG" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // YUV aliases | ||||||
|  |  | ||||||
|  | // FormatYUYV is an alias of FormatYUY2 | ||||||
| const FormatYUYV = FormatYUY2 | const FormatYUYV = FormatYUY2 | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewDecoder(f Format) (Decoder, error) { | func NewDecoder(f Format) (Decoder, error) { | ||||||
| 	var decoder decoderFunc | 	var decoder DecoderFunc | ||||||
|  |  | ||||||
| 	switch f { | 	switch f { | ||||||
| 	case FormatI420: | 	case FormatI420: | ||||||
|   | |||||||
| @@ -3,12 +3,12 @@ package frame | |||||||
| import "image" | import "image" | ||||||
|  |  | ||||||
| type Decoder interface { | 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 | // 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) | 	return f(frame, width, height) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,13 +5,13 @@ import ( | |||||||
| 	"image" | 	"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 | 	yi := width * height | ||||||
| 	cbi := yi + width*height/4 | 	cbi := yi + width*height/4 | ||||||
| 	cri := cbi + width*height/4 | 	cri := cbi + width*height/4 | ||||||
|  |  | ||||||
| 	if cri > len(frame) { | 	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{ | 	return &image.YCbCr{ | ||||||
| @@ -22,15 +22,15 @@ func decodeI420(frame []byte, width, height int) (image.Image, func(), error) { | |||||||
| 		CStride:        width / 2, | 		CStride:        width / 2, | ||||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio420, | 		SubsampleRatio: image.YCbCrSubsampleRatio420, | ||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		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 | 	yi := width * height | ||||||
| 	ci := yi + width*height/2 | 	ci := yi + width*height/2 | ||||||
|  |  | ||||||
| 	if ci > len(frame) { | 	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 | 	var cb, cr []byte | ||||||
| @@ -47,5 +47,5 @@ func decodeNV21(frame []byte, width, height int) (image.Image, func(), error) { | |||||||
| 		CStride:        width / 2, | 		CStride:        width / 2, | ||||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio420, | 		SubsampleRatio: image.YCbCrSubsampleRatio420, | ||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		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); | // void decodeUYVYCGO(uint8_t* y, uint8_t* cb, uint8_t* cr, uint8_t* uyvy, int width, int height); | ||||||
| import "C" | 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 | 	yi := width * height | ||||||
| 	ci := yi / 2 | 	ci := yi / 2 | ||||||
| 	fi := yi + 2*ci | 	fi := yi + 2*ci | ||||||
|  |  | ||||||
| 	if len(frame) != fi { | 	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) | 	y := make([]byte, yi) | ||||||
| @@ -41,16 +41,16 @@ func decodeYUY2(frame []byte, width, height int) (image.Image, func(), error) { | |||||||
| 		CStride:        width / 2, | 		CStride:        width / 2, | ||||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		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 | 	yi := width * height | ||||||
| 	ci := yi / 2 | 	ci := yi / 2 | ||||||
| 	fi := yi + 2*ci | 	fi := yi + 2*ci | ||||||
|  |  | ||||||
| 	if len(frame) != fi { | 	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) | 	y := make([]byte, yi) | ||||||
| @@ -73,5 +73,5 @@ func decodeUYVY(frame []byte, width, height int) (image.Image, func(), error) { | |||||||
| 		CStride:        width / 2, | 		CStride:        width / 2, | ||||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		Rect:           image.Rect(0, 0, width, height), | ||||||
| 	}, func() {}, nil | 	}, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,13 +7,13 @@ import ( | |||||||
| 	"image" | 	"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 | 	yi := width * height | ||||||
| 	ci := yi / 2 | 	ci := yi / 2 | ||||||
| 	fi := yi + 2*ci | 	fi := yi + 2*ci | ||||||
|  |  | ||||||
| 	if len(frame) != fi { | 	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) | 	y := make([]byte, yi) | ||||||
| @@ -39,16 +39,16 @@ func decodeYUY2(frame []byte, width, height int) (image.Image, func(), error) { | |||||||
| 		CStride:        width / 2, | 		CStride:        width / 2, | ||||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		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 | 	yi := width * height | ||||||
| 	ci := yi / 2 | 	ci := yi / 2 | ||||||
| 	fi := yi + 2*ci | 	fi := yi + 2*ci | ||||||
|  |  | ||||||
| 	if len(frame) != fi { | 	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) | 	y := make([]byte, yi) | ||||||
| @@ -74,5 +74,5 @@ func decodeUYVY(frame []byte, width, height int) (image.Image, func(), error) { | |||||||
| 		CStride:        width / 2, | 		CStride:        width / 2, | ||||||
| 		SubsampleRatio: image.YCbCrSubsampleRatio422, | 		SubsampleRatio: image.YCbCrSubsampleRatio422, | ||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		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), | 		Rect:           image.Rect(0, 0, width, height), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	img, _, err := decodeYUY2(input, width, height) | 	img, err := decodeYUY2(input, width, height) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| @@ -56,7 +56,7 @@ func TestDecodeUYVY(t *testing.T) { | |||||||
| 		Rect:           image.Rect(0, 0, width, height), | 		Rect:           image.Rect(0, 0, width, height), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	img, _, err := decodeUYVY(input, width, height) | 	img, err := decodeUYVY(input, width, height) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		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) { | 		b.Run(fmt.Sprintf("%dx%d", sz.width, sz.height), func(b *testing.B) { | ||||||
| 			input := make([]byte, sz.width*sz.height*2) | 			input := make([]byte, sz.width*sz.height*2) | ||||||
| 			for i := 0; i < b.N; i++ { | 			for i := 0; i < b.N; i++ { | ||||||
| 				_, _, err := decodeYUY2(input, sz.width, sz.height) | 				_, err := decodeYUY2(input, sz.width, sz.height) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					b.Fatal(err) | 					b.Fatal(err) | ||||||
| 				} | 				} | ||||||
|   | |||||||
| @@ -5,14 +5,13 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Reader interface { | 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) { | func (rf ReaderFunc) Read() (wave.Audio, error) { | ||||||
| 	chunk, release, err = rf() | 	return rf() | ||||||
| 	return |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // TransformFunc produces a new Reader that will produces a transformed audio | // 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 | 	var inBuff wave.Audio | ||||||
|  |  | ||||||
| 	return func(r Reader) Reader { | 	return func(r Reader) Reader { | ||||||
| 		return ReaderFunc(func() (wave.Audio, func(), error) { | 		return ReaderFunc(func() (wave.Audio, error) { | ||||||
| 			for { | 			for { | ||||||
| 				if inBuff != nil && inBuff.ChunkInfo().Len >= nSamples { | 				if inBuff != nil && inBuff.ChunkInfo().Len >= nSamples { | ||||||
| 					break | 					break | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				buff, _, err := r.Read() | 				buff, err := r.Read() | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					return nil, func() {}, err | 					return nil, err | ||||||
| 				} | 				} | ||||||
| 				switch b := buff.(type) { | 				switch b := buff.(type) { | ||||||
| 				case *wave.Float32Interleaved: | 				case *wave.Float32Interleaved: | ||||||
| @@ -59,7 +59,7 @@ func NewBuffer(nSamples int) TransformFunc { | |||||||
| 					ib.Size.Len += b.Size.Len | 					ib.Size.Len += b.Size.Len | ||||||
|  |  | ||||||
| 				default: | 				default: | ||||||
| 					return nil, func() {}, errUnsupported | 					return nil, errUnsupported | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			switch ib := inBuff.(type) { | 			switch ib := inBuff.(type) { | ||||||
| @@ -71,7 +71,7 @@ func NewBuffer(nSamples int) TransformFunc { | |||||||
| 				copy(ibCopy.Data, ib.Data) | 				copy(ibCopy.Data, ib.Data) | ||||||
| 				ib.Data = ib.Data[n:] | 				ib.Data = ib.Data[n:] | ||||||
| 				ib.Size.Len -= nSamples | 				ib.Size.Len -= nSamples | ||||||
| 				return &ibCopy, func() {}, nil | 				return &ibCopy, nil | ||||||
|  |  | ||||||
| 			case *wave.Float32Interleaved: | 			case *wave.Float32Interleaved: | ||||||
| 				ibCopy := *ib | 				ibCopy := *ib | ||||||
| @@ -81,9 +81,9 @@ func NewBuffer(nSamples int) TransformFunc { | |||||||
| 				copy(ibCopy.Data, ib.Data) | 				copy(ibCopy.Data, ib.Data) | ||||||
| 				ib.Data = ib.Data[n:] | 				ib.Data = ib.Data[n:] | ||||||
| 				ib.Size.Len -= nSamples | 				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) | 	trans := NewBuffer(3) | ||||||
|  |  | ||||||
| 	var iSent int | 	var iSent int | ||||||
| 	r := trans(ReaderFunc(func() (wave.Audio, func(), error) { | 	r := trans(ReaderFunc(func() (wave.Audio, error) { | ||||||
| 		if iSent < len(input) { | 		if iSent < len(input) { | ||||||
| 			iSent++ | 			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++ { | 	for i := 0; ; i++ { | ||||||
| 		a, _, err := r.Read() | 		a, err := r.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			if err == io.EOF && i >= len(expected) { | 			if err == io.EOF && i >= len(expected) { | ||||||
| 				break | 				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. | // NewChannelMixer creates audio transform to mix audio channels. | ||||||
| func NewChannelMixer(channels int, mixer mixer.ChannelMixer) TransformFunc { | func NewChannelMixer(channels int, mixer mixer.ChannelMixer) TransformFunc { | ||||||
| 	return func(r Reader) Reader { | 	return func(r Reader) Reader { | ||||||
| 		return ReaderFunc(func() (wave.Audio, func(), error) { | 		return ReaderFunc(func() (wave.Audio, error) { | ||||||
| 			buff, _, err := r.Read() | 			buff, err := r.Read() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, func() {}, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
| 			ci := buff.ChunkInfo() | 			ci := buff.ChunkInfo() | ||||||
| 			if ci.Channels == channels { | 			if ci.Channels == channels { | ||||||
| 				return buff, func() {}, nil | 				return buff, nil | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			ci.Channels = channels | 			ci.Channels = channels | ||||||
| @@ -32,9 +32,9 @@ func NewChannelMixer(channels int, mixer mixer.ChannelMixer) TransformFunc { | |||||||
| 				mixed = wave.NewFloat32NonInterleaved(ci) | 				mixed = wave.NewFloat32NonInterleaved(ci) | ||||||
| 			} | 			} | ||||||
| 			if err := mixer.Mix(mixed, buff); err != nil { | 			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{}) | 	trans := NewChannelMixer(1, &mixer.MonoMixer{}) | ||||||
|  |  | ||||||
| 	var iSent int | 	var iSent int | ||||||
| 	r := trans(ReaderFunc(func() (wave.Audio, func(), error) { | 	r := trans(ReaderFunc(func() (wave.Audio, error) { | ||||||
| 		if iSent < len(input) { | 		if iSent < len(input) { | ||||||
| 			iSent++ | 			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++ { | 	for i := 0; ; i++ { | ||||||
| 		a, _, err := r.Read() | 		a, err := r.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			if err == io.EOF && i >= len(expected) { | 			if err == io.EOF && i >= len(expected) { | ||||||
| 				break | 				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 | // ToI420 converts r to a new reader that will output images in I420 format | ||||||
| func ToI420(r Reader) Reader { | func ToI420(r Reader) Reader { | ||||||
| 	var yuvImg image.YCbCr | 	var yuvImg image.YCbCr | ||||||
| 	return ReaderFunc(func() (image.Image, func(), error) { | 	return ReaderFunc(func() (image.Image, error) { | ||||||
| 		img, _, err := r.Read() | 		img, err := r.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, func() {}, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		imageToYCbCr(&yuvImg, img) | 		imageToYCbCr(&yuvImg, img) | ||||||
| @@ -79,11 +79,11 @@ func ToI420(r Reader) Reader { | |||||||
| 			i422ToI420(&yuvImg) | 			i422ToI420(&yuvImg) | ||||||
| 		case image.YCbCrSubsampleRatio420: | 		case image.YCbCrSubsampleRatio420: | ||||||
| 		default: | 		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 | 		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 | // ToRGBA converts r to a new reader that will output images in RGBA format | ||||||
| func ToRGBA(r Reader) Reader { | func ToRGBA(r Reader) Reader { | ||||||
| 	var dst image.RGBA | 	var dst image.RGBA | ||||||
| 	return ReaderFunc(func() (image.Image, func(), error) { | 	return ReaderFunc(func() (image.Image, error) { | ||||||
| 		img, _, err := r.Read() | 		img, err := r.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, func() {}, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		imageToRGBA(&dst, img) | 		imageToRGBA(&dst, img) | ||||||
| 		return &dst, func() {}, nil | 		return &dst, nil | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -144,10 +144,10 @@ func TestToI420(t *testing.T) { | |||||||
| 	for name, c := range cases { | 	for name, c := range cases { | ||||||
| 		c := c | 		c := c | ||||||
| 		t.Run(name, func(t *testing.T) { | 		t.Run(name, func(t *testing.T) { | ||||||
| 			r := ToI420(ReaderFunc(func() (image.Image, func(), error) { | 			r := ToI420(ReaderFunc(func() (image.Image, error) { | ||||||
| 				return c.src, func() {}, nil | 				return c.src, nil | ||||||
| 			})) | 			})) | ||||||
| 			out, _, err := r.Read() | 			out, err := r.Read() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatalf("Unexpected error: %v", err) | 				t.Fatalf("Unexpected error: %v", err) | ||||||
| 			} | 			} | ||||||
| @@ -199,10 +199,10 @@ func TestToRGBA(t *testing.T) { | |||||||
| 	for name, c := range cases { | 	for name, c := range cases { | ||||||
| 		c := c | 		c := c | ||||||
| 		t.Run(name, func(t *testing.T) { | 		t.Run(name, func(t *testing.T) { | ||||||
| 			r := ToRGBA(ReaderFunc(func() (image.Image, func(), error) { | 			r := ToRGBA(ReaderFunc(func() (image.Image, error) { | ||||||
| 				return c.src, func() {}, nil | 				return c.src, nil | ||||||
| 			})) | 			})) | ||||||
| 			out, _, err := r.Read() | 			out, err := r.Read() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatalf("Unexpected error: %v", err) | 				t.Fatalf("Unexpected error: %v", err) | ||||||
| 			} | 			} | ||||||
| @@ -225,12 +225,12 @@ func BenchmarkToI420(b *testing.B) { | |||||||
| 			for name, img := range cases { | 			for name, img := range cases { | ||||||
| 				img := img | 				img := img | ||||||
| 				b.Run(name, func(b *testing.B) { | 				b.Run(name, func(b *testing.B) { | ||||||
| 					r := ToI420(ReaderFunc(func() (image.Image, func(), error) { | 					r := ToI420(ReaderFunc(func() (image.Image, error) { | ||||||
| 						return img, func() {}, nil | 						return img, nil | ||||||
| 					})) | 					})) | ||||||
|  |  | ||||||
| 					for i := 0; i < b.N; i++ { | 					for i := 0; i < b.N; i++ { | ||||||
| 						_, _, err := r.Read() | 						_, err := r.Read() | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							b.Fatalf("Unexpected error: %v", err) | 							b.Fatalf("Unexpected error: %v", err) | ||||||
| 						} | 						} | ||||||
| @@ -253,12 +253,12 @@ func BenchmarkToRGBA(b *testing.B) { | |||||||
| 			for name, img := range cases { | 			for name, img := range cases { | ||||||
| 				img := img | 				img := img | ||||||
| 				b.Run(name, func(b *testing.B) { | 				b.Run(name, func(b *testing.B) { | ||||||
| 					r := ToRGBA(ReaderFunc(func() (image.Image, func(), error) { | 					r := ToRGBA(ReaderFunc(func() (image.Image, error) { | ||||||
| 						return img, func() {}, nil | 						return img, nil | ||||||
| 					})) | 					})) | ||||||
|  |  | ||||||
| 					for i := 0; i < b.N; i++ { | 					for i := 0; i < b.N; i++ { | ||||||
| 						_, _, err := r.Read() | 						_, err := r.Read() | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							b.Fatalf("Unexpected error: %v", err) | 							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 currentProp prop.Media | ||||||
| 		var lastTaken time.Time | 		var lastTaken time.Time | ||||||
| 		var frames uint | 		var frames uint | ||||||
| 		return ReaderFunc(func() (image.Image, func(), error) { | 		return ReaderFunc(func() (image.Image, error) { | ||||||
| 			var dirty bool | 			var dirty bool | ||||||
|  |  | ||||||
| 			img, _, err := r.Read() | 			img, err := r.Read() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, func() {}, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			bounds := img.Bounds() | 			bounds := img.Bounds() | ||||||
| @@ -52,7 +52,7 @@ func DetectChanges(interval time.Duration, onChange func(prop.Media)) TransformF | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			frames++ | 			frames++ | ||||||
| 			return img, func() {}, nil | 			return img, nil | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,8 +12,8 @@ import ( | |||||||
|  |  | ||||||
| func BenchmarkDetectChanges(b *testing.B) { | func BenchmarkDetectChanges(b *testing.B) { | ||||||
| 	var src Reader | 	var src Reader | ||||||
| 	src = ReaderFunc(func() (image.Image, func(), error) { | 	src = ReaderFunc(func() (image.Image, error) { | ||||||
| 		return image.NewRGBA(image.Rect(0, 0, 1920, 1080)), func() {}, nil | 		return image.NewRGBA(image.Rect(0, 0, 1920, 1080)), nil | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	b.Run("WithoutDetectChanges", func(b *testing.B) { | 	b.Run("WithoutDetectChanges", func(b *testing.B) { | ||||||
| @@ -40,8 +40,8 @@ func BenchmarkDetectChanges(b *testing.B) { | |||||||
|  |  | ||||||
| func TestDetectChanges(t *testing.T) { | func TestDetectChanges(t *testing.T) { | ||||||
| 	buildSource := func(p prop.Media) (Reader, func(prop.Media)) { | 	buildSource := func(p prop.Media) (Reader, func(prop.Media)) { | ||||||
| 		return ReaderFunc(func() (image.Image, func(), error) { | 		return ReaderFunc(func() (image.Image, error) { | ||||||
| 				return image.NewRGBA(image.Rect(0, 0, p.Width, p.Height)), func() {}, nil | 				return image.NewRGBA(image.Rect(0, 0, p.Width, p.Height)), nil | ||||||
| 			}), func(newProp prop.Media) { | 			}), func(newProp prop.Media) { | ||||||
| 				p = newProp | 				p = newProp | ||||||
| 			} | 			} | ||||||
| @@ -86,7 +86,7 @@ func TestDetectChanges(t *testing.T) { | |||||||
| 			detectBeforeFirstFrame = true | 			detectBeforeFirstFrame = true | ||||||
| 		})(src) | 		})(src) | ||||||
|  |  | ||||||
| 		frame, _, err := src.Read() | 		frame, err := src.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatal(err) | 			t.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @@ -113,7 +113,7 @@ func TestDetectChanges(t *testing.T) { | |||||||
| 				expected.Width = width | 				expected.Width = width | ||||||
| 				expected.Height = height | 				expected.Height = height | ||||||
| 				update(expected) | 				update(expected) | ||||||
| 				frame, _, err := src.Read() | 				frame, err := src.Read() | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					t.Fatal(err) | 					t.Fatal(err) | ||||||
| 				} | 				} | ||||||
| @@ -143,7 +143,7 @@ func TestDetectChanges(t *testing.T) { | |||||||
| 		})(src) | 		})(src) | ||||||
|  |  | ||||||
| 		for count < 3 { | 		for count < 3 { | ||||||
| 			frame, _, err := src.Read() | 			frame, err := src.Read() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Fatal(err) | 				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) { | 		return ReaderFunc(func() (image.Image, error) { | ||||||
| 			img, _, err := r.Read() | 			img, err := r.Read() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return nil, func() {}, err | 				return nil, err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			switch v := img.(type) { | 			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) | 				scalerCached.Scale(dst, rect, v, v.Rect, draw.Src, nil) | ||||||
|  |  | ||||||
| 				cloned := *dst // clone metadata | 				cloned := *dst // clone metadata | ||||||
| 				return &cloned, func() {}, nil | 				return &cloned, nil | ||||||
|  |  | ||||||
| 			case *image.YCbCr: | 			case *image.YCbCr: | ||||||
| 				ycbcrRealloc(v) | 				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) | 				scalerCached.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Src, nil) | ||||||
|  |  | ||||||
| 				cloned := *(imgScaled.(*image.YCbCr)) // clone metadata | 				cloned := *(imgScaled.(*image.YCbCr)) // clone metadata | ||||||
| 				return &cloned, func() {}, nil | 				return &cloned, nil | ||||||
|  |  | ||||||
| 			default: | 			default: | ||||||
| 				return nil, func() {}, errUnsupportedImageType | 				return nil, errUnsupportedImageType | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -215,11 +215,11 @@ func TestScale(t *testing.T) { | |||||||
| 				c := c | 				c := c | ||||||
| 				t.Run(name, func(t *testing.T) { | 				t.Run(name, func(t *testing.T) { | ||||||
| 					trans := Scale(c.width, c.height, algo) | 					trans := Scale(c.width, c.height, algo) | ||||||
| 					r := trans(ReaderFunc(func() (image.Image, func(), error) { | 					r := trans(ReaderFunc(func() (image.Image, error) { | ||||||
| 						return c.src, func() {}, nil | 						return c.src, nil | ||||||
| 					})) | 					})) | ||||||
| 					for i := 0; i < 4; i++ { | 					for i := 0; i < 4; i++ { | ||||||
| 						out, _, err := r.Read() | 						out, err := r.Read() | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							t.Fatalf("Unexpected error: %v", err) | 							t.Fatalf("Unexpected error: %v", err) | ||||||
| 						} | 						} | ||||||
| @@ -261,12 +261,12 @@ func BenchmarkScale(b *testing.B) { | |||||||
| 						img := img | 						img := img | ||||||
| 						b.Run(name, func(b *testing.B) { | 						b.Run(name, func(b *testing.B) { | ||||||
| 							trans := Scale(640, 360, algo) | 							trans := Scale(640, 360, algo) | ||||||
| 							r := trans(ReaderFunc(func() (image.Image, func(), error) { | 							r := trans(ReaderFunc(func() (image.Image, error) { | ||||||
| 								return img, func() {}, nil | 								return img, nil | ||||||
| 							})) | 							})) | ||||||
|  |  | ||||||
| 							for i := 0; i < b.N; i++ { | 							for i := 0; i < b.N; i++ { | ||||||
| 								_, _, err := r.Read() | 								_, err := r.Read() | ||||||
| 								if err != nil { | 								if err != nil { | ||||||
| 									b.Fatalf("Unexpected error: %v", err) | 									b.Fatalf("Unexpected error: %v", err) | ||||||
| 								} | 								} | ||||||
|   | |||||||
| @@ -10,16 +10,16 @@ import ( | |||||||
| func Throttle(rate float32) TransformFunc { | func Throttle(rate float32) TransformFunc { | ||||||
| 	return func(r Reader) Reader { | 	return func(r Reader) Reader { | ||||||
| 		ticker := time.NewTicker(time.Duration(int64(float64(time.Second) / float64(rate)))) | 		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 { | 			for { | ||||||
| 				img, _, err := r.Read() | 				img, err := r.Read() | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					ticker.Stop() | 					ticker.Stop() | ||||||
| 					return nil, func() {}, err | 					return nil, err | ||||||
| 				} | 				} | ||||||
| 				select { | 				select { | ||||||
| 				case <-ticker.C: | 				case <-ticker.C: | ||||||
| 					return img, func() {}, nil | 					return img, nil | ||||||
| 				default: | 				default: | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -19,14 +19,14 @@ func TestThrottle(t *testing.T) { | |||||||
|  |  | ||||||
| 	var cntPush int | 	var cntPush int | ||||||
| 	trans := Throttle(50) | 	trans := Throttle(50) | ||||||
| 	r := trans(ReaderFunc(func() (image.Image, func(), error) { | 	r := trans(ReaderFunc(func() (image.Image, error) { | ||||||
| 		<-ticker.C | 		<-ticker.C | ||||||
| 		cntPush++ | 		cntPush++ | ||||||
| 		return img, func() {}, nil | 		return img, nil | ||||||
| 	})) | 	})) | ||||||
|  |  | ||||||
| 	for i := 0; i < 20; i++ { | 	for i := 0; i < 20; i++ { | ||||||
| 		_, _, err := r.Read() | 		_, err := r.Read() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("Unexpected error: %v", err) | 			t.Fatalf("Unexpected error: %v", err) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -5,14 +5,13 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| type Reader interface { | 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) { | func (rf ReaderFunc) Read() (img image.Image, err error) { | ||||||
| 	img, release, err = rf() | 	return rf() | ||||||
| 	return |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // TransformFunc produces a new Reader that will produces a transformed video | // TransformFunc produces a new Reader that will produces a transformed video | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| package prop | package prop | ||||||
|  |  | ||||||
| import "fmt" |  | ||||||
|  |  | ||||||
| // BoolConstraint is an interface to represent bool value constraint. | // BoolConstraint is an interface to represent bool value constraint. | ||||||
| type BoolConstraint interface { | type BoolConstraint interface { | ||||||
| 	Compare(bool) (float64, bool) | 	Compare(bool) (float64, bool) | ||||||
| @@ -22,11 +20,6 @@ func (b BoolExact) Compare(o bool) (float64, bool) { | |||||||
| // Value implements BoolConstraint. | // Value implements BoolConstraint. | ||||||
| func (b BoolExact) Value() bool { return bool(b) } | 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. | // Bool specifies ideal bool value. | ||||||
| type Bool BoolExact | type Bool BoolExact | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,7 @@ | |||||||
| package prop | package prop | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"math" | 	"math" | ||||||
| 	"strings" |  | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -25,11 +23,6 @@ func (d Duration) Compare(a time.Duration) (float64, bool) { | |||||||
| // Value implements DurationConstraint. | // Value implements DurationConstraint. | ||||||
| func (d Duration) Value() (time.Duration, bool) { return time.Duration(d), true } | 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. | // DurationExact specifies exact duration value. | ||||||
| type DurationExact time.Duration | type DurationExact time.Duration | ||||||
|  |  | ||||||
| @@ -44,11 +37,6 @@ func (d DurationExact) Compare(a time.Duration) (float64, bool) { | |||||||
| // Value implements DurationConstraint. | // Value implements DurationConstraint. | ||||||
| func (d DurationExact) Value() (time.Duration, bool) { return time.Duration(d), true } | 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. | // DurationOneOf specifies list of expected duration values. | ||||||
| type DurationOneOf []time.Duration | type DurationOneOf []time.Duration | ||||||
|  |  | ||||||
| @@ -65,16 +53,6 @@ func (d DurationOneOf) Compare(a time.Duration) (float64, bool) { | |||||||
| // Value implements DurationConstraint. | // Value implements DurationConstraint. | ||||||
| func (DurationOneOf) Value() (time.Duration, bool) { return 0, false } | 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. | // DurationRanged specifies range of expected duration value. | ||||||
| // If Ideal is non-zero, closest value to Ideal takes priority. | // If Ideal is non-zero, closest value to Ideal takes priority. | ||||||
| type DurationRanged struct { | type DurationRanged struct { | ||||||
| @@ -118,8 +96,3 @@ func (d DurationRanged) Compare(a time.Duration) (float64, bool) { | |||||||
|  |  | ||||||
| // Value implements DurationConstraint. | // Value implements DurationConstraint. | ||||||
| func (DurationRanged) Value() (time.Duration, bool) { return 0, false } | 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 | package prop | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"math" | 	"math" | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // FloatConstraint is an interface to represent float value constraint. | // FloatConstraint is an interface to represent float value constraint. | ||||||
| @@ -24,11 +22,6 @@ func (f Float) Compare(a float32) (float64, bool) { | |||||||
| // Value implements FloatConstraint. | // Value implements FloatConstraint. | ||||||
| func (f Float) Value() (float32, bool) { return float32(f), true } | 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. | // FloatExact specifies exact float value. | ||||||
| type FloatExact float32 | type FloatExact float32 | ||||||
|  |  | ||||||
| @@ -43,11 +36,6 @@ func (f FloatExact) Compare(a float32) (float64, bool) { | |||||||
| // Value implements FloatConstraint. | // Value implements FloatConstraint. | ||||||
| func (f FloatExact) Value() (float32, bool) { return float32(f), true } | 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. | // FloatOneOf specifies list of expected float values. | ||||||
| type FloatOneOf []float32 | type FloatOneOf []float32 | ||||||
|  |  | ||||||
| @@ -64,16 +52,6 @@ func (f FloatOneOf) Compare(a float32) (float64, bool) { | |||||||
| // Value implements FloatConstraint. | // Value implements FloatConstraint. | ||||||
| func (FloatOneOf) Value() (float32, bool) { return 0, false } | 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. | // FloatRanged specifies range of expected float value. | ||||||
| // If Ideal is non-zero, closest value to Ideal takes priority. | // If Ideal is non-zero, closest value to Ideal takes priority. | ||||||
| type FloatRanged struct { | type FloatRanged struct { | ||||||
| @@ -117,8 +95,3 @@ func (f FloatRanged) Compare(a float32) (float64, bool) { | |||||||
|  |  | ||||||
| // Value implements FloatConstraint. | // Value implements FloatConstraint. | ||||||
| func (FloatRanged) Value() (float32, bool) { return 0, false } | 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 | package prop | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"github.com/pion/mediadevices/pkg/frame" | 	"github.com/pion/mediadevices/pkg/frame" | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // FrameFormatConstraint is an interface to represent frame format constraint. | // 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. | // Value implements FrameFormatConstraint. | ||||||
| func (f FrameFormat) Value() (frame.Format, bool) { return frame.Format(f), true } | 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. | // FrameFormatExact specifies exact frame format. | ||||||
| type FrameFormatExact frame.Format | type FrameFormatExact frame.Format | ||||||
|  |  | ||||||
| @@ -46,11 +39,6 @@ func (f FrameFormatExact) Compare(a frame.Format) (float64, bool) { | |||||||
| // Value implements FrameFormatConstraint. | // Value implements FrameFormatConstraint. | ||||||
| func (f FrameFormatExact) Value() (frame.Format, bool) { return frame.Format(f), true } | 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. | // FrameFormatOneOf specifies list of expected frame format. | ||||||
| type FrameFormatOneOf []frame.Format | type FrameFormatOneOf []frame.Format | ||||||
|  |  | ||||||
| @@ -66,13 +54,3 @@ func (f FrameFormatOneOf) Compare(a frame.Format) (float64, bool) { | |||||||
|  |  | ||||||
| // Value implements FrameFormatConstraint. | // Value implements FrameFormatConstraint. | ||||||
| func (FrameFormatOneOf) Value() (frame.Format, bool) { return "", false } | 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 | package prop | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"math" | 	"math" | ||||||
| 	"strings" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // IntConstraint is an interface to represent integer value constraint. | // IntConstraint is an interface to represent integer value constraint. | ||||||
| @@ -24,11 +22,6 @@ func (i Int) Compare(a int) (float64, bool) { | |||||||
| // Value implements IntConstraint. | // Value implements IntConstraint. | ||||||
| func (i Int) Value() (int, bool) { return int(i), true } | 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. | // IntExact specifies exact int value. | ||||||
| type IntExact int | type IntExact int | ||||||
|  |  | ||||||
| @@ -40,11 +33,6 @@ func (i IntExact) Compare(a int) (float64, bool) { | |||||||
| 	return 1.0, false | 	return 1.0, false | ||||||
| } | } | ||||||
|  |  | ||||||
| // String implements Stringify |  | ||||||
| func (i IntExact) String() string { |  | ||||||
| 	return fmt.Sprintf("%d (exact)", i) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Value implements IntConstraint. | // Value implements IntConstraint. | ||||||
| func (i IntExact) Value() (int, bool) { return int(i), true } | 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. | // Value implements IntConstraint. | ||||||
| func (IntOneOf) Value() (int, bool) { return 0, false } | 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. | // IntRanged specifies range of expected int value. | ||||||
| // If Ideal is non-zero, closest value to Ideal takes priority. | // If Ideal is non-zero, closest value to Ideal takes priority. | ||||||
| type IntRanged struct { | type IntRanged struct { | ||||||
| @@ -117,8 +95,3 @@ func (i IntRanged) Compare(a int) (float64, bool) { | |||||||
|  |  | ||||||
| // Value implements IntConstraint. | // Value implements IntConstraint. | ||||||
| func (IntRanged) Value() (int, bool) { return 0, false } | 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 | package prop | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"strings" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/frame" | 	"github.com/pion/mediadevices/pkg/frame" | ||||||
| @@ -17,10 +15,6 @@ type MediaConstraints struct { | |||||||
| 	AudioConstraints | 	AudioConstraints | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *MediaConstraints) String() string { |  | ||||||
| 	return prettifyStruct(m) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Media stores single set of media propaties. | // Media stores single set of media propaties. | ||||||
| type Media struct { | type Media struct { | ||||||
| 	DeviceID string | 	DeviceID string | ||||||
| @@ -28,40 +22,6 @@ type Media struct { | |||||||
| 	Audio | 	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 | // setterFn is a callback function to set value from fieldB to fieldA | ||||||
| type setterFn func(fieldA, fieldB reflect.Value) | 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") | 		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 | package prop | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // StringConstraint is an interface to represent string constraint. | // StringConstraint is an interface to represent string constraint. | ||||||
| type StringConstraint interface { | type StringConstraint interface { | ||||||
| 	Compare(string) (float64, bool) | 	Compare(string) (float64, bool) | ||||||
| @@ -26,11 +21,6 @@ func (f String) Compare(a string) (float64, bool) { | |||||||
| // Value implements StringConstraint. | // Value implements StringConstraint. | ||||||
| func (f String) Value() (string, bool) { return string(f), true } | 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. | // StringExact specifies exact string. | ||||||
| type StringExact string | type StringExact string | ||||||
|  |  | ||||||
| @@ -45,11 +35,6 @@ func (f StringExact) Compare(a string) (float64, bool) { | |||||||
| // Value implements StringConstraint. | // Value implements StringConstraint. | ||||||
| func (f StringExact) Value() (string, bool) { return string(f), true } | 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. | // StringOneOf specifies list of expected string. | ||||||
| type StringOneOf []string | type StringOneOf []string | ||||||
|  |  | ||||||
| @@ -65,8 +50,3 @@ func (f StringOneOf) Compare(a string) (float64, bool) { | |||||||
|  |  | ||||||
| // Value implements StringConstraint. | // Value implements StringConstraint. | ||||||
| func (StringOneOf) Value() (string, bool) { return "", false } | 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 | package mediadevices | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"fmt" | ||||||
| 	"image" | 	"image" | ||||||
| 	"math/rand" |  | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/pion/mediadevices/pkg/codec" |  | ||||||
| 	"github.com/pion/mediadevices/pkg/driver" | 	"github.com/pion/mediadevices/pkg/driver" | ||||||
| 	"github.com/pion/mediadevices/pkg/io/audio" | 	"github.com/pion/mediadevices/pkg/io/audio" | ||||||
| 	"github.com/pion/mediadevices/pkg/io/video" | 	"github.com/pion/mediadevices/pkg/io/video" | ||||||
| 	"github.com/pion/mediadevices/pkg/wave" | 	"github.com/pion/mediadevices/pkg/wave" | ||||||
| 	"github.com/pion/webrtc/v2" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | // TrackKind represents content type of a track | ||||||
| 	errInvalidDriverType      = errors.New("invalid driver type") | type TrackKind string | ||||||
| 	errNotFoundPeerConnection = errors.New("failed to find given peer connection") |  | ||||||
|  | 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 | // Track is an interface that represent MediaStreamTrack | ||||||
| // Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack | // Reference: https://w3c.github.io/mediacapture-main/#mediastreamtrack | ||||||
| type Track interface { | type Track interface { | ||||||
| 	Source | 	ID() string | ||||||
|  | 	Kind() TrackKind | ||||||
|  | 	Stop() | ||||||
| 	// OnEnded registers a handler to receive an error from the media stream track. | 	// 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 | 	// If the error is already occured before registering, the handler will be | ||||||
| 	// immediately called. | 	// immediately called. | ||||||
| 	OnEnded(func(error)) | 	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 { | // VideoTrack is a specialized track for video | ||||||
| 	Source | type VideoTrack struct { | ||||||
| 	err                   error | 	baseTrack | ||||||
| 	onErrorHandler        func(error) | 	src         video.Reader | ||||||
| 	mu                    sync.Mutex | 	transformed video.Reader | ||||||
| 	endOnce               sync.Once | 	mux         sync.Mutex | ||||||
| 	kind                  MediaDeviceType | 	frameCount  int | ||||||
| 	selector              *CodecSelector | 	lastFrame   image.Image | ||||||
| 	activePeerConnections map[*webrtc.PeerConnection]chan<- chan<- struct{} | 	lastErr     error | ||||||
| } | } | ||||||
|  |  | ||||||
| func newBaseTrack(source Source, kind MediaDeviceType, selector *CodecSelector) *baseTrack { | func newVideoTrack(d driver.Driver, constraints MediaTrackConstraints) (*VideoTrack, error) { | ||||||
| 	return &baseTrack{ | 	err := d.Open() | ||||||
| 		Source:                source, | 	if err != nil { | ||||||
| 		kind:                  kind, | 		return nil, err | ||||||
| 		selector:              selector, |  | ||||||
| 		activePeerConnections: make(map[*webrtc.PeerConnection]chan<- chan<- struct{}), |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	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 | // Kind returns track's kind | ||||||
| func (track *baseTrack) Kind() MediaDeviceType { | func (track *VideoTrack) Kind() TrackKind { | ||||||
| 	return track.kind | 	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 | // 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. | // error occurs, handler will get called with the error given to the parameter. | ||||||
| func (track *baseTrack) OnEnded(handler func(error)) { | func (t *baseTrack) OnEnded(handler func(error)) { | ||||||
| 	track.mu.Lock() | 	t.mu.Lock() | ||||||
| 	track.onErrorHandler = handler | 	t.onErrorHandler = handler | ||||||
| 	err := track.err | 	err := t.err | ||||||
| 	track.mu.Unlock() | 	t.mu.Unlock() | ||||||
|  |  | ||||||
| 	if err != nil && handler != nil { | 	if err != nil && handler != nil { | ||||||
| 		// Already errored. | 		// Already errored. | ||||||
| 		track.endOnce.Do(func() { | 		t.endOnce.Do(func() { | ||||||
| 			handler(err) | 			handler(err) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // onError is a callback when an error occurs | // onError is a callback when an error occurs | ||||||
| func (track *baseTrack) onError(err error) { | func (t *baseTrack) onError(err error) { | ||||||
| 	track.mu.Lock() | 	t.mu.Lock() | ||||||
| 	track.err = err | 	t.err = err | ||||||
| 	handler := track.onErrorHandler | 	handler := t.onErrorHandler | ||||||
| 	track.mu.Unlock() | 	t.mu.Unlock() | ||||||
|  |  | ||||||
| 	if handler != nil { | 	if handler != nil { | ||||||
| 		track.endOnce.Do(func() { | 		t.endOnce.Do(func() { | ||||||
| 			handler(err) | 			handler(err) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (track *baseTrack) bind(pc *webrtc.PeerConnection, encodedReader codec.ReadCloser, selectedCodec *codec.RTPCodec, sampler func(*webrtc.Track) samplerFunc) (*webrtc.Track, error) { | func (t *baseTrack) Stop() { | ||||||
| 	track.mu.Lock() | 	t.d.Close() | ||||||
| 	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) |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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