├── .github └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── compile.sh ├── transcoder.pb.go └── transcoder.proto ├── cmd └── main.go ├── examples ├── internal │ └── signal │ │ └── signal.go ├── transcode-file │ ├── README.md │ ├── input.ivf │ ├── main.go │ └── output.ivf └── transcode-mediadevice │ ├── README.md │ ├── jsfiddle │ ├── demo.css │ ├── demo.details │ ├── demo.html │ └── demo.js │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── av │ ├── avcodec.go │ ├── decode.go │ ├── demux.c │ ├── demux.go │ ├── demux.h │ ├── encode.go │ ├── error.go │ ├── mux.c │ ├── mux.go │ ├── mux.h │ ├── temp_sdp.go │ ├── transcoder.go │ └── types.go ├── codecs │ ├── codecs.go │ └── packetizer.go └── transcoder │ ├── client.go │ └── server.go ├── test ├── e2e_test.go ├── input.h265 ├── input.ivf ├── input.ogg ├── output.avi ├── output.h264 ├── output.h265 ├── output.ivf ├── output.ogg └── video.h265 └── video264.sdp /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | release: 8 | types: [released] 9 | pull_request: 10 | branches: 11 | - 'main' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | 27 | - name: Log in to the Container registry 28 | uses: docker/login-action@v1 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Extract metadata (tags, labels) for Docker 35 | id: meta 36 | uses: docker/metadata-action@v3 37 | with: 38 | images: ghcr.io/${{ github.repository }} 39 | tags: | 40 | type=ref,event=branch 41 | type=ref,event=pr 42 | type=semver,pattern={{version}} 43 | type=semver,pattern={{major}}.{{minor}} 44 | 45 | - name: Build and push Docker image 46 | uses: docker/build-push-action@v2 47 | with: 48 | context: . 49 | push: true 50 | tags: ${{ steps.meta.outputs.tags }} 51 | labels: ${{ steps.meta.outputs.labels }} 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max 54 | 55 | deploy: 56 | if: github.ref == 'refs/heads/main' 57 | runs-on: ubuntu-latest 58 | needs: build 59 | concurrency: 60 | group: deploy 61 | cancel-in-progress: true 62 | 63 | steps: 64 | - uses: google-github-actions/auth@v0 65 | with: 66 | credentials_json: ${{ secrets.TRANSCODER_SERVICE_ACCOUNT_JSON }} 67 | 68 | - name: Set up Cloud SDK 69 | uses: google-github-actions/setup-gcloud@v0 70 | 71 | - name: Deploy to GCE 72 | id: deploy 73 | run: | 74 | gcloud compute instance-groups managed rolling-action start-update transcoder --version=template=transcoder --region=us-east4 75 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '15 12 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "stdint.h": "c" 4 | } 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | FROM golang:1.17-alpine 4 | 5 | RUN apk add ffmpeg 6 | 7 | WORKDIR /app 8 | 9 | COPY go.* ./ 10 | 11 | RUN go mod download 12 | 13 | COPY . ./ 14 | 15 | RUN go build -v -o /transcoder cmd/main.go 16 | 17 | ENV APP_ENV=production 18 | 19 | EXPOSE 5000-5200/udp 20 | EXPOSE 50051/tcp 21 | 22 | CMD [ "/transcoder" ] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # muxable/transcoder 2 | 3 | This package implements a transcoding server. It accepts WebRTC signalled over gRPC and will respond with a transcoded track for each received track. 4 | 5 | **Are you interested in livestreaming video with WebRTC?** Join us at [Muxable](mailto:kevin@muxable.com)! 6 | 7 | ## Client SDK 8 | 9 | For convenience if you don't want to deal with signalling and reconnection semantics, a lightweight client SDK is provided. 10 | 11 | ### Example usage 12 | 13 | ```go 14 | import "github.com/muxable/transcoder/pkg/transcoder" 15 | 16 | ... 17 | 18 | conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | client, err := transcoder.NewClient(context.Background(), conn) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | transcoded, err := client.Transcode(track) 29 | if err != nil { 30 | return err 31 | } 32 | ``` 33 | 34 | Here, `client.Transcode()` accepts a `TrackLocal` and returns a `TrackRemote`. The mime type of the output track can also be specified, for example: 35 | 36 | ```go 37 | transcoded, err := client.Transcode(track, transcode.ToMimeType(webrtc.MimeTypeVP9)) 38 | ``` 39 | 40 | ## Cloud hosting 41 | 42 | If you are interested in using this service pre-deployed, please email kevin@muxable.com. We are exploring offering endpoints as a paid service. 43 | 44 | ## Debugging 45 | 46 | The transcoding server uses GStreamer under the hood. To view debug messages from GStreamer, set `GST_DEBUG=3`. -------------------------------------------------------------------------------- /api/compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -v $PWD:/defs namely/protoc-all -f transcoder.proto -l go 4 | -------------------------------------------------------------------------------- /api/transcoder.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.18.1 5 | // source: transcoder.proto 6 | 7 | package api 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 15 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 16 | anypb "google.golang.org/protobuf/types/known/anypb" 17 | reflect "reflect" 18 | sync "sync" 19 | ) 20 | 21 | const ( 22 | // Verify that this generated code is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 24 | // Verify that runtime/protoimpl is sufficiently up-to-date. 25 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 26 | ) 27 | 28 | type TranscodeRequest struct { 29 | state protoimpl.MessageState 30 | sizeCache protoimpl.SizeCache 31 | unknownFields protoimpl.UnknownFields 32 | 33 | StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` 34 | TrackId string `protobuf:"bytes,2,opt,name=track_id,json=trackId,proto3" json:"track_id,omitempty"` 35 | RtpStreamId string `protobuf:"bytes,3,opt,name=rtp_stream_id,json=rtpStreamId,proto3" json:"rtp_stream_id,omitempty"` 36 | MimeType string `protobuf:"bytes,4,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"` 37 | GstreamerPipeline string `protobuf:"bytes,5,opt,name=gstreamer_pipeline,json=gstreamerPipeline,proto3" json:"gstreamer_pipeline,omitempty"` 38 | } 39 | 40 | func (x *TranscodeRequest) Reset() { 41 | *x = TranscodeRequest{} 42 | if protoimpl.UnsafeEnabled { 43 | mi := &file_transcoder_proto_msgTypes[0] 44 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 45 | ms.StoreMessageInfo(mi) 46 | } 47 | } 48 | 49 | func (x *TranscodeRequest) String() string { 50 | return protoimpl.X.MessageStringOf(x) 51 | } 52 | 53 | func (*TranscodeRequest) ProtoMessage() {} 54 | 55 | func (x *TranscodeRequest) ProtoReflect() protoreflect.Message { 56 | mi := &file_transcoder_proto_msgTypes[0] 57 | if protoimpl.UnsafeEnabled && x != nil { 58 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 59 | if ms.LoadMessageInfo() == nil { 60 | ms.StoreMessageInfo(mi) 61 | } 62 | return ms 63 | } 64 | return mi.MessageOf(x) 65 | } 66 | 67 | // Deprecated: Use TranscodeRequest.ProtoReflect.Descriptor instead. 68 | func (*TranscodeRequest) Descriptor() ([]byte, []int) { 69 | return file_transcoder_proto_rawDescGZIP(), []int{0} 70 | } 71 | 72 | func (x *TranscodeRequest) GetStreamId() string { 73 | if x != nil { 74 | return x.StreamId 75 | } 76 | return "" 77 | } 78 | 79 | func (x *TranscodeRequest) GetTrackId() string { 80 | if x != nil { 81 | return x.TrackId 82 | } 83 | return "" 84 | } 85 | 86 | func (x *TranscodeRequest) GetRtpStreamId() string { 87 | if x != nil { 88 | return x.RtpStreamId 89 | } 90 | return "" 91 | } 92 | 93 | func (x *TranscodeRequest) GetMimeType() string { 94 | if x != nil { 95 | return x.MimeType 96 | } 97 | return "" 98 | } 99 | 100 | func (x *TranscodeRequest) GetGstreamerPipeline() string { 101 | if x != nil { 102 | return x.GstreamerPipeline 103 | } 104 | return "" 105 | } 106 | 107 | type SubscribeRequest struct { 108 | state protoimpl.MessageState 109 | sizeCache protoimpl.SizeCache 110 | unknownFields protoimpl.UnknownFields 111 | 112 | // Types that are assignable to Operation: 113 | // *SubscribeRequest_Request 114 | // *SubscribeRequest_Signal 115 | Operation isSubscribeRequest_Operation `protobuf_oneof:"operation"` 116 | } 117 | 118 | func (x *SubscribeRequest) Reset() { 119 | *x = SubscribeRequest{} 120 | if protoimpl.UnsafeEnabled { 121 | mi := &file_transcoder_proto_msgTypes[1] 122 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 123 | ms.StoreMessageInfo(mi) 124 | } 125 | } 126 | 127 | func (x *SubscribeRequest) String() string { 128 | return protoimpl.X.MessageStringOf(x) 129 | } 130 | 131 | func (*SubscribeRequest) ProtoMessage() {} 132 | 133 | func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { 134 | mi := &file_transcoder_proto_msgTypes[1] 135 | if protoimpl.UnsafeEnabled && x != nil { 136 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 137 | if ms.LoadMessageInfo() == nil { 138 | ms.StoreMessageInfo(mi) 139 | } 140 | return ms 141 | } 142 | return mi.MessageOf(x) 143 | } 144 | 145 | // Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. 146 | func (*SubscribeRequest) Descriptor() ([]byte, []int) { 147 | return file_transcoder_proto_rawDescGZIP(), []int{1} 148 | } 149 | 150 | func (m *SubscribeRequest) GetOperation() isSubscribeRequest_Operation { 151 | if m != nil { 152 | return m.Operation 153 | } 154 | return nil 155 | } 156 | 157 | func (x *SubscribeRequest) GetRequest() *TranscodeRequest { 158 | if x, ok := x.GetOperation().(*SubscribeRequest_Request); ok { 159 | return x.Request 160 | } 161 | return nil 162 | } 163 | 164 | func (x *SubscribeRequest) GetSignal() *anypb.Any { 165 | if x, ok := x.GetOperation().(*SubscribeRequest_Signal); ok { 166 | return x.Signal 167 | } 168 | return nil 169 | } 170 | 171 | type isSubscribeRequest_Operation interface { 172 | isSubscribeRequest_Operation() 173 | } 174 | 175 | type SubscribeRequest_Request struct { 176 | Request *TranscodeRequest `protobuf:"bytes,1,opt,name=request,proto3,oneof"` 177 | } 178 | 179 | type SubscribeRequest_Signal struct { 180 | Signal *anypb.Any `protobuf:"bytes,2,opt,name=signal,proto3,oneof"` 181 | } 182 | 183 | func (*SubscribeRequest_Request) isSubscribeRequest_Operation() {} 184 | 185 | func (*SubscribeRequest_Signal) isSubscribeRequest_Operation() {} 186 | 187 | var File_transcoder_proto protoreflect.FileDescriptor 188 | 189 | var file_transcoder_proto_rawDesc = []byte{ 190 | 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 191 | 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 192 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 193 | 0x74, 0x6f, 0x22, 0xba, 0x01, 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x6f, 0x64, 0x65, 194 | 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 195 | 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x72, 0x65, 196 | 0x61, 0x6d, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 197 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x49, 0x64, 0x12, 198 | 0x22, 0x0a, 0x0d, 0x72, 0x74, 0x70, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 199 | 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x74, 0x70, 0x53, 0x74, 0x72, 0x65, 0x61, 200 | 0x6d, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 201 | 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 202 | 0x12, 0x2d, 0x0a, 0x12, 0x67, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x65, 0x72, 0x5f, 0x70, 0x69, 203 | 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x67, 0x73, 204 | 0x74, 0x72, 0x65, 0x61, 0x6d, 0x65, 0x72, 0x50, 0x69, 0x70, 0x65, 0x6c, 0x69, 0x6e, 0x65, 0x22, 205 | 0x82, 0x01, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 206 | 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 207 | 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x72, 0x61, 0x6e, 208 | 0x73, 0x63, 0x6f, 0x64, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x07, 209 | 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 210 | 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 211 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x48, 0x00, 0x52, 212 | 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x42, 0x0b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 213 | 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x89, 0x01, 0x0a, 0x0a, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x6f, 214 | 0x64, 0x65, 0x72, 0x12, 0x3b, 0x0a, 0x07, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x12, 0x14, 215 | 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 216 | 0x2e, 0x41, 0x6e, 0x79, 0x1a, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 217 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 218 | 0x12, 0x3e, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x15, 0x2e, 219 | 0x61, 0x70, 0x69, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 220 | 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 221 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 222 | 0x42, 0x23, 0x5a, 0x21, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 223 | 0x75, 0x78, 0x61, 0x62, 0x6c, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x63, 0x6f, 0x64, 0x65, 224 | 0x72, 0x2f, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 225 | } 226 | 227 | var ( 228 | file_transcoder_proto_rawDescOnce sync.Once 229 | file_transcoder_proto_rawDescData = file_transcoder_proto_rawDesc 230 | ) 231 | 232 | func file_transcoder_proto_rawDescGZIP() []byte { 233 | file_transcoder_proto_rawDescOnce.Do(func() { 234 | file_transcoder_proto_rawDescData = protoimpl.X.CompressGZIP(file_transcoder_proto_rawDescData) 235 | }) 236 | return file_transcoder_proto_rawDescData 237 | } 238 | 239 | var file_transcoder_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 240 | var file_transcoder_proto_goTypes = []interface{}{ 241 | (*TranscodeRequest)(nil), // 0: api.TranscodeRequest 242 | (*SubscribeRequest)(nil), // 1: api.SubscribeRequest 243 | (*anypb.Any)(nil), // 2: google.protobuf.Any 244 | } 245 | var file_transcoder_proto_depIdxs = []int32{ 246 | 0, // 0: api.SubscribeRequest.request:type_name -> api.TranscodeRequest 247 | 2, // 1: api.SubscribeRequest.signal:type_name -> google.protobuf.Any 248 | 2, // 2: api.Transcoder.Publish:input_type -> google.protobuf.Any 249 | 1, // 3: api.Transcoder.Subscribe:input_type -> api.SubscribeRequest 250 | 2, // 4: api.Transcoder.Publish:output_type -> google.protobuf.Any 251 | 2, // 5: api.Transcoder.Subscribe:output_type -> google.protobuf.Any 252 | 4, // [4:6] is the sub-list for method output_type 253 | 2, // [2:4] is the sub-list for method input_type 254 | 2, // [2:2] is the sub-list for extension type_name 255 | 2, // [2:2] is the sub-list for extension extendee 256 | 0, // [0:2] is the sub-list for field type_name 257 | } 258 | 259 | func init() { file_transcoder_proto_init() } 260 | func file_transcoder_proto_init() { 261 | if File_transcoder_proto != nil { 262 | return 263 | } 264 | if !protoimpl.UnsafeEnabled { 265 | file_transcoder_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 266 | switch v := v.(*TranscodeRequest); i { 267 | case 0: 268 | return &v.state 269 | case 1: 270 | return &v.sizeCache 271 | case 2: 272 | return &v.unknownFields 273 | default: 274 | return nil 275 | } 276 | } 277 | file_transcoder_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 278 | switch v := v.(*SubscribeRequest); i { 279 | case 0: 280 | return &v.state 281 | case 1: 282 | return &v.sizeCache 283 | case 2: 284 | return &v.unknownFields 285 | default: 286 | return nil 287 | } 288 | } 289 | } 290 | file_transcoder_proto_msgTypes[1].OneofWrappers = []interface{}{ 291 | (*SubscribeRequest_Request)(nil), 292 | (*SubscribeRequest_Signal)(nil), 293 | } 294 | type x struct{} 295 | out := protoimpl.TypeBuilder{ 296 | File: protoimpl.DescBuilder{ 297 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 298 | RawDescriptor: file_transcoder_proto_rawDesc, 299 | NumEnums: 0, 300 | NumMessages: 2, 301 | NumExtensions: 0, 302 | NumServices: 1, 303 | }, 304 | GoTypes: file_transcoder_proto_goTypes, 305 | DependencyIndexes: file_transcoder_proto_depIdxs, 306 | MessageInfos: file_transcoder_proto_msgTypes, 307 | }.Build() 308 | File_transcoder_proto = out.File 309 | file_transcoder_proto_rawDesc = nil 310 | file_transcoder_proto_goTypes = nil 311 | file_transcoder_proto_depIdxs = nil 312 | } 313 | 314 | // Reference imports to suppress errors if they are not otherwise used. 315 | var _ context.Context 316 | var _ grpc.ClientConnInterface 317 | 318 | // This is a compile-time assertion to ensure that this generated file 319 | // is compatible with the grpc package it is being compiled against. 320 | const _ = grpc.SupportPackageIsVersion6 321 | 322 | // TranscoderClient is the client API for Transcoder service. 323 | // 324 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 325 | type TranscoderClient interface { 326 | Publish(ctx context.Context, opts ...grpc.CallOption) (Transcoder_PublishClient, error) 327 | Subscribe(ctx context.Context, opts ...grpc.CallOption) (Transcoder_SubscribeClient, error) 328 | } 329 | 330 | type transcoderClient struct { 331 | cc grpc.ClientConnInterface 332 | } 333 | 334 | func NewTranscoderClient(cc grpc.ClientConnInterface) TranscoderClient { 335 | return &transcoderClient{cc} 336 | } 337 | 338 | func (c *transcoderClient) Publish(ctx context.Context, opts ...grpc.CallOption) (Transcoder_PublishClient, error) { 339 | stream, err := c.cc.NewStream(ctx, &_Transcoder_serviceDesc.Streams[0], "/api.Transcoder/Publish", opts...) 340 | if err != nil { 341 | return nil, err 342 | } 343 | x := &transcoderPublishClient{stream} 344 | return x, nil 345 | } 346 | 347 | type Transcoder_PublishClient interface { 348 | Send(*anypb.Any) error 349 | Recv() (*anypb.Any, error) 350 | grpc.ClientStream 351 | } 352 | 353 | type transcoderPublishClient struct { 354 | grpc.ClientStream 355 | } 356 | 357 | func (x *transcoderPublishClient) Send(m *anypb.Any) error { 358 | return x.ClientStream.SendMsg(m) 359 | } 360 | 361 | func (x *transcoderPublishClient) Recv() (*anypb.Any, error) { 362 | m := new(anypb.Any) 363 | if err := x.ClientStream.RecvMsg(m); err != nil { 364 | return nil, err 365 | } 366 | return m, nil 367 | } 368 | 369 | func (c *transcoderClient) Subscribe(ctx context.Context, opts ...grpc.CallOption) (Transcoder_SubscribeClient, error) { 370 | stream, err := c.cc.NewStream(ctx, &_Transcoder_serviceDesc.Streams[1], "/api.Transcoder/Subscribe", opts...) 371 | if err != nil { 372 | return nil, err 373 | } 374 | x := &transcoderSubscribeClient{stream} 375 | return x, nil 376 | } 377 | 378 | type Transcoder_SubscribeClient interface { 379 | Send(*SubscribeRequest) error 380 | Recv() (*anypb.Any, error) 381 | grpc.ClientStream 382 | } 383 | 384 | type transcoderSubscribeClient struct { 385 | grpc.ClientStream 386 | } 387 | 388 | func (x *transcoderSubscribeClient) Send(m *SubscribeRequest) error { 389 | return x.ClientStream.SendMsg(m) 390 | } 391 | 392 | func (x *transcoderSubscribeClient) Recv() (*anypb.Any, error) { 393 | m := new(anypb.Any) 394 | if err := x.ClientStream.RecvMsg(m); err != nil { 395 | return nil, err 396 | } 397 | return m, nil 398 | } 399 | 400 | // TranscoderServer is the server API for Transcoder service. 401 | type TranscoderServer interface { 402 | Publish(Transcoder_PublishServer) error 403 | Subscribe(Transcoder_SubscribeServer) error 404 | } 405 | 406 | // UnimplementedTranscoderServer can be embedded to have forward compatible implementations. 407 | type UnimplementedTranscoderServer struct { 408 | } 409 | 410 | func (*UnimplementedTranscoderServer) Publish(Transcoder_PublishServer) error { 411 | return status.Errorf(codes.Unimplemented, "method Publish not implemented") 412 | } 413 | func (*UnimplementedTranscoderServer) Subscribe(Transcoder_SubscribeServer) error { 414 | return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") 415 | } 416 | 417 | func RegisterTranscoderServer(s *grpc.Server, srv TranscoderServer) { 418 | s.RegisterService(&_Transcoder_serviceDesc, srv) 419 | } 420 | 421 | func _Transcoder_Publish_Handler(srv interface{}, stream grpc.ServerStream) error { 422 | return srv.(TranscoderServer).Publish(&transcoderPublishServer{stream}) 423 | } 424 | 425 | type Transcoder_PublishServer interface { 426 | Send(*anypb.Any) error 427 | Recv() (*anypb.Any, error) 428 | grpc.ServerStream 429 | } 430 | 431 | type transcoderPublishServer struct { 432 | grpc.ServerStream 433 | } 434 | 435 | func (x *transcoderPublishServer) Send(m *anypb.Any) error { 436 | return x.ServerStream.SendMsg(m) 437 | } 438 | 439 | func (x *transcoderPublishServer) Recv() (*anypb.Any, error) { 440 | m := new(anypb.Any) 441 | if err := x.ServerStream.RecvMsg(m); err != nil { 442 | return nil, err 443 | } 444 | return m, nil 445 | } 446 | 447 | func _Transcoder_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { 448 | return srv.(TranscoderServer).Subscribe(&transcoderSubscribeServer{stream}) 449 | } 450 | 451 | type Transcoder_SubscribeServer interface { 452 | Send(*anypb.Any) error 453 | Recv() (*SubscribeRequest, error) 454 | grpc.ServerStream 455 | } 456 | 457 | type transcoderSubscribeServer struct { 458 | grpc.ServerStream 459 | } 460 | 461 | func (x *transcoderSubscribeServer) Send(m *anypb.Any) error { 462 | return x.ServerStream.SendMsg(m) 463 | } 464 | 465 | func (x *transcoderSubscribeServer) Recv() (*SubscribeRequest, error) { 466 | m := new(SubscribeRequest) 467 | if err := x.ServerStream.RecvMsg(m); err != nil { 468 | return nil, err 469 | } 470 | return m, nil 471 | } 472 | 473 | var _Transcoder_serviceDesc = grpc.ServiceDesc{ 474 | ServiceName: "api.Transcoder", 475 | HandlerType: (*TranscoderServer)(nil), 476 | Methods: []grpc.MethodDesc{}, 477 | Streams: []grpc.StreamDesc{ 478 | { 479 | StreamName: "Publish", 480 | Handler: _Transcoder_Publish_Handler, 481 | ServerStreams: true, 482 | ClientStreams: true, 483 | }, 484 | { 485 | StreamName: "Subscribe", 486 | Handler: _Transcoder_Subscribe_Handler, 487 | ServerStreams: true, 488 | ClientStreams: true, 489 | }, 490 | }, 491 | Metadata: "transcoder.proto", 492 | } 493 | -------------------------------------------------------------------------------- /api/transcoder.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "github.com/muxable/transcoder/api"; 4 | 5 | import "google/protobuf/any.proto"; 6 | 7 | package api; 8 | 9 | service Transcoder { 10 | rpc Publish(stream google.protobuf.Any) returns (stream google.protobuf.Any) {} 11 | rpc Subscribe(stream SubscribeRequest) returns (stream google.protobuf.Any) {} 12 | } 13 | 14 | message TranscodeRequest { 15 | string stream_id = 1; 16 | string track_id = 2; 17 | string rtp_stream_id = 3; 18 | 19 | string mime_type = 4; 20 | string gstreamer_pipeline = 5; 21 | } 22 | 23 | message SubscribeRequest { 24 | oneof operation { 25 | TranscodeRequest request = 1; 26 | google.protobuf.Any signal = 2; 27 | } 28 | } -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "os" 6 | 7 | "github.com/blendle/zapdriver" 8 | "github.com/muxable/transcoder/pkg/av" 9 | "github.com/pion/rtp" 10 | "github.com/pion/webrtc/v3" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | /* 15 | import ( 16 | "flag" 17 | "net" 18 | "os" 19 | 20 | "github.com/blendle/zapdriver" 21 | "github.com/muxable/transcoder/api" 22 | "github.com/muxable/transcoder/pkg/transcoder" 23 | "github.com/pion/webrtc/v3" 24 | "go.uber.org/zap" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/health" 27 | "google.golang.org/grpc/health/grpc_health_v1" 28 | ) 29 | 30 | func logger() (*zap.Logger, error) { 31 | if os.Getenv("APP_ENV") == "production" { 32 | return zapdriver.NewProduction() 33 | } else { 34 | return zap.NewDevelopment() 35 | } 36 | } 37 | 38 | func main() { 39 | addr := flag.String("addr", ":50051", "The address to listen on") 40 | flag.Parse() 41 | 42 | logger, err := logger() 43 | if err != nil { 44 | panic(err) 45 | } 46 | defer logger.Sync() 47 | undo := zap.ReplaceGlobals(logger) 48 | defer undo() 49 | 50 | port := os.Getenv("PORT") 51 | if port == "" { 52 | port = "50051" 53 | } 54 | 55 | lis, err := net.Listen("tcp", *addr) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | s := grpc.NewServer() 61 | 62 | api.RegisterTranscoderServer(s, transcoder.NewTranscoderServer(webrtc.Configuration{ 63 | ICEServers: []webrtc.ICEServer{ 64 | {URLs: []string{"stun:stun.l.google.com:19302"}}, 65 | }, 66 | })) 67 | grpc_health_v1.RegisterHealthServer(s, health.NewServer()) 68 | 69 | zap.L().Info("starting transcoder server", zap.String("addr", *addr)) 70 | 71 | if err := s.Serve(lis); err != nil { 72 | panic(err) 73 | } 74 | } 75 | */ 76 | 77 | func logger() (*zap.Logger, error) { 78 | if os.Getenv("APP_ENV") == "production" { 79 | return zapdriver.NewProduction() 80 | } else { 81 | return zap.NewDevelopment() 82 | } 83 | } 84 | 85 | func main() { 86 | logger, err := logger() 87 | if err != nil { 88 | panic(err) 89 | } 90 | defer logger.Sync() 91 | undo := zap.ReplaceGlobals(logger) 92 | defer undo() 93 | 94 | tc, err := av.NewTranscoder(webrtc.RTPCodecParameters{ 95 | PayloadType: 96, 96 | RTPCodecCapability: webrtc.RTPCodecCapability{ 97 | MimeType: "video/H265", 98 | ClockRate: 90000, 99 | }, 100 | }, 101 | webrtc.RTPCodecCapability{ 102 | MimeType: "video/H264", 103 | ClockRate: 90000, 104 | }) 105 | 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | zap.L().Info("starting transcoder server") 111 | 112 | conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 5000}) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | dial, err := net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 5001}) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | go func() { 123 | buf := make([]byte, 1500) 124 | defer tc.Close() 125 | for { 126 | n, err := conn.Read(buf) 127 | if err != nil { 128 | return 129 | } 130 | p := &rtp.Packet{} 131 | if err := p.Unmarshal(buf[:n]); err != nil { 132 | panic(err) 133 | } 134 | if err := tc.WriteRTP(p); err != nil { 135 | panic(err) 136 | } 137 | } 138 | }() 139 | 140 | for { 141 | p, err := tc.ReadRTP() 142 | if err != nil { 143 | return 144 | } 145 | buf, err := p.Marshal() 146 | if err != nil { 147 | panic(err) 148 | } 149 | if _, err := dial.Write(buf); err != nil { 150 | panic(err) 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /examples/internal/signal/signal.go: -------------------------------------------------------------------------------- 1 | // Package signal contains helpers to exchange the SDP session 2 | // description between examples. 3 | package signal 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | // Allows compressing offer/answer to bypass terminal input limits. 19 | const compress = false 20 | 21 | // MustReadStdin blocks until input is received from stdin 22 | func MustReadStdin() string { 23 | r := bufio.NewReader(os.Stdin) 24 | 25 | var in string 26 | for { 27 | var err error 28 | in, err = r.ReadString('\n') 29 | if err != io.EOF { 30 | if err != nil { 31 | panic(err) 32 | } 33 | } 34 | in = strings.TrimSpace(in) 35 | if len(in) > 0 { 36 | break 37 | } 38 | } 39 | 40 | fmt.Println("") 41 | 42 | return in 43 | } 44 | 45 | // Encode encodes the input in base64 46 | // It can optionally zip the input before encoding 47 | func Encode(obj interface{}) string { 48 | b, err := json.Marshal(obj) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | if compress { 54 | b = zip(b) 55 | } 56 | 57 | return base64.StdEncoding.EncodeToString(b) 58 | } 59 | 60 | // Decode decodes the input from base64 61 | // It can optionally unzip the input after decoding 62 | func Decode(in string, obj interface{}) { 63 | b, err := base64.StdEncoding.DecodeString(in) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | if compress { 69 | b = unzip(b) 70 | } 71 | 72 | err = json.Unmarshal(b, obj) 73 | if err != nil { 74 | panic(err) 75 | } 76 | } 77 | 78 | func zip(in []byte) []byte { 79 | var b bytes.Buffer 80 | gz := gzip.NewWriter(&b) 81 | _, err := gz.Write(in) 82 | if err != nil { 83 | panic(err) 84 | } 85 | err = gz.Flush() 86 | if err != nil { 87 | panic(err) 88 | } 89 | err = gz.Close() 90 | if err != nil { 91 | panic(err) 92 | } 93 | return b.Bytes() 94 | } 95 | 96 | func unzip(in []byte) []byte { 97 | var b bytes.Buffer 98 | _, err := b.Write(in) 99 | if err != nil { 100 | panic(err) 101 | } 102 | r, err := gzip.NewReader(&b) 103 | if err != nil { 104 | panic(err) 105 | } 106 | res, err := ioutil.ReadAll(r) 107 | if err != nil { 108 | panic(err) 109 | } 110 | return res 111 | } 112 | -------------------------------------------------------------------------------- /examples/transcode-file/README.md: -------------------------------------------------------------------------------- 1 | # transcode-file 2 | transcode-file is a simple application that shows how to transcode an ivf file on disk. 3 | 4 | ## Instructions 5 | ### Run 6 | 7 | ```bash 8 | go run main.go -addr localhost:50051 -i input.ivf -o output.ivf 9 | ``` 10 | 11 | You should see an `output.ivf` file produced. 12 | -------------------------------------------------------------------------------- /examples/transcode-file/input.ivf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/examples/transcode-file/input.ivf -------------------------------------------------------------------------------- /examples/transcode-file/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "io" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/muxable/transcoder/pkg/transcoder" 12 | "github.com/pion/webrtc/v3" 13 | "github.com/pion/webrtc/v3/pkg/media" 14 | "github.com/pion/webrtc/v3/pkg/media/ivfreader" 15 | "github.com/pion/webrtc/v3/pkg/media/ivfwriter" 16 | "go.uber.org/zap" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/credentials/insecure" 19 | ) 20 | 21 | func fileToVideoTrack(filename string, tl *webrtc.TrackLocalStaticSample) { 22 | file, ivfErr := os.Open(filename) 23 | if ivfErr != nil { 24 | panic(ivfErr) 25 | } 26 | 27 | ivf, header, ivfErr := ivfreader.NewWith(file) 28 | if ivfErr != nil { 29 | panic(ivfErr) 30 | } 31 | 32 | dt := time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*1000) 33 | ticker := time.NewTicker(dt) 34 | for ; true; <-ticker.C { 35 | frame, _, ivfErr := ivf.ParseNextFrame() 36 | if ivfErr == io.EOF { 37 | os.Exit(0) 38 | } 39 | 40 | if ivfErr != nil { 41 | panic(ivfErr) 42 | } 43 | 44 | if ivfErr = tl.WriteSample(media.Sample{Data: frame, Duration: dt}); ivfErr != nil { 45 | panic(ivfErr) 46 | } 47 | } 48 | } 49 | 50 | func videoTrackToFile(tr *webrtc.TrackRemote, filename string) { 51 | ivfFile, err := ivfwriter.New(filename) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | defer func() { 57 | if err := ivfFile.Close(); err != nil { 58 | panic(err) 59 | } 60 | }() 61 | 62 | for { 63 | rtpPacket, _, err := tr.ReadRTP() 64 | if err != nil { 65 | panic(err) 66 | } 67 | if err := ivfFile.WriteRTP(rtpPacket); err != nil { 68 | panic(err) 69 | } 70 | } 71 | } 72 | 73 | func main() { 74 | addr := flag.String("addr", "localhost:50051", "the address to connect to") 75 | input := flag.String("i", "input.ivf", "the input file") 76 | output := flag.String("o", "output.ivf", "the output file") 77 | flag.Parse() 78 | 79 | logger, err := zap.NewDevelopment() 80 | if err != nil { 81 | log.Fatalf("can't initialize zap logger: %v", err) 82 | } 83 | zap.ReplaceGlobals(logger) 84 | 85 | // create tracks 86 | videoTrack, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8}, "video", "pion") 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | client, err := transcoder.NewClient(context.Background(), conn) 97 | if err != nil { 98 | panic(err) 99 | } 100 | 101 | go fileToVideoTrack(*input, videoTrack) 102 | 103 | transcodedVideoTrack, err := client.Transcode(videoTrack, transcoder.ToMimeType(webrtc.MimeTypeVP8)) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | go videoTrackToFile(transcodedVideoTrack, *output) 109 | 110 | select {} 111 | } 112 | -------------------------------------------------------------------------------- /examples/transcode-file/output.ivf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/examples/transcode-file/output.ivf -------------------------------------------------------------------------------- /examples/transcode-mediadevice/README.md: -------------------------------------------------------------------------------- 1 | # transcode-mediadevice 2 | transcode-mediadevice is an example application showing a webcam stream transcoded to different formats. 3 | 4 | ## Instructions 5 | ### Open transcode-mediadevice example page 6 | [jsfiddle.net](https://jsfiddle.net/nbpfwry3/4/) you should see your Webcam, two text-areas and a 'Start Session' button 7 | 8 | ### Run transcode-mediadevice with your browsers SessionDescription as stdin 9 | In the jsfiddle the top textarea is your browser, copy that and: 10 | #### Linux/macOS 11 | Run `echo $BROWSER_SDP | go run main.go` 12 | #### Windows 13 | 1. Paste the SessionDescription into a file. 14 | 1. Run `go run main.go < my_file` 15 | 16 | ### Input transcode-mediadevice's SessionDescription into your browser 17 | Copy the text that `transcode-mediadevice` just emitted and copy into second text area 18 | 19 | ### Hit 'Start Session' in jsfiddle, view the transcoded stream! 20 | The output stream will be transcoded to H264, regardless of what input codec is used. -------------------------------------------------------------------------------- /examples/transcode-mediadevice/jsfiddle/demo.css: -------------------------------------------------------------------------------- 1 | textarea { 2 | width: 500px; 3 | min-height: 75px; 4 | } -------------------------------------------------------------------------------- /examples/transcode-mediadevice/jsfiddle/demo.details: -------------------------------------------------------------------------------- 1 | --- 2 | name: transcoder 3 | description: Example of how to transcode tracks with a WebRTC transcoding server. 4 | authors: 5 | - Kevin Wang -------------------------------------------------------------------------------- /examples/transcode-mediadevice/jsfiddle/demo.html: -------------------------------------------------------------------------------- 1 | 2 | Browser base64 Session Description
3 |
4 | 5 | Golang base64 Session Description
6 |
7 |
8 | 9 |
10 | 11 |
12 | Browser stream
13 | 14 |
15 | 16 |
17 | Video from server
18 |
-------------------------------------------------------------------------------- /examples/transcode-mediadevice/jsfiddle/demo.js: -------------------------------------------------------------------------------- 1 | // Create peer conn 2 | const pc = new RTCPeerConnection({ 3 | iceServers: [ 4 | { 5 | urls: "stun:stun.l.google.com:19302", 6 | }, 7 | ], 8 | }); 9 | 10 | pc.oniceconnectionstatechange = (e) => { 11 | console.log("connection state change", pc.iceConnectionState); 12 | }; 13 | pc.onicecandidate = (event) => { 14 | if (event.candidate === null) { 15 | document.getElementById("localSessionDescription").value = btoa( 16 | JSON.stringify(pc.localDescription) 17 | ); 18 | } 19 | }; 20 | 21 | pc.onnegotiationneeded = (e) => 22 | pc 23 | .createOffer() 24 | .then((d) => pc.setLocalDescription(d)) 25 | .catch(console.error); 26 | 27 | pc.ontrack = (event) => { 28 | console.log("Got track event", event); 29 | let video = document.createElement("video"); 30 | video.srcObject = event.streams[0]; 31 | video.autoplay = true; 32 | video.width = "500"; 33 | let label = document.createElement("div"); 34 | label.textContent = event.streams[0].id; 35 | document.getElementById("serverVideos").appendChild(label); 36 | document.getElementById("serverVideos").appendChild(video); 37 | }; 38 | 39 | navigator.mediaDevices 40 | .getUserMedia({ 41 | video: { 42 | width: { 43 | ideal: 4096, 44 | }, 45 | height: { 46 | ideal: 2160, 47 | }, 48 | frameRate: { 49 | ideal: 60, 50 | min: 10, 51 | }, 52 | }, 53 | audio: false, 54 | }) 55 | .then((stream) => { 56 | document.getElementById("browserVideo").srcObject = stream; 57 | pc.addTransceiver(stream.getVideoTracks()[0], { 58 | direction: "sendonly", 59 | streams: [stream], 60 | sendEncodings: [ 61 | // for firefox order matters... first high resolution, then scaled resolutions... 62 | { 63 | rid: "f", 64 | }, 65 | { 66 | rid: "h", 67 | scaleResolutionDownBy: 2.0, 68 | }, 69 | { 70 | rid: "q", 71 | scaleResolutionDownBy: 4.0, 72 | }, 73 | ], 74 | }); 75 | pc.addTransceiver("video"); 76 | pc.addTransceiver("video"); 77 | pc.addTransceiver("video"); 78 | }); 79 | 80 | window.startSession = () => { 81 | const sd = document.getElementById("remoteSessionDescription").value; 82 | if (sd === "") { 83 | return alert("Session Description must not be empty"); 84 | } 85 | 86 | try { 87 | console.log("answer", JSON.parse(atob(sd))); 88 | pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd)))); 89 | } catch (e) { 90 | alert(e); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /examples/transcode-mediadevice/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | 12 | "github.com/muxable/transcoder/examples/internal/signal" 13 | "github.com/muxable/transcoder/pkg/transcoder" 14 | "github.com/pion/webrtc/v3" 15 | "go.uber.org/zap" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/credentials/insecure" 18 | ) 19 | 20 | // The transcoder API typically requires a bidi streaming gRPC connection which is difficult 21 | // to replicate on the browser (at least, not without a decent amount of infrastructure). Since 22 | // transcoding directly on from a browser isn't a common use case, we'll use a simple SDP exchange 23 | // to broker the connection. In practice, your code will look much simpler if a track is originating 24 | // from a server. 25 | 26 | func main() { 27 | logger, err := zap.NewDevelopment() 28 | if err != nil { 29 | log.Fatalf("can't initialize zap logger: %v", err) 30 | } 31 | zap.ReplaceGlobals(logger) 32 | 33 | addr := flag.String("addr", "localhost:50051", "transcoder grpc addr") 34 | 35 | flag.Parse() 36 | 37 | // Create a new transcoding client. 38 | conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | tc, err := transcoder.NewClient(context.Background(), conn) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // Prepare the configuration 49 | config := webrtc.Configuration{ 50 | ICEServers: []webrtc.ICEServer{ 51 | { 52 | URLs: []string{"stun:stun.l.google.com:19302"}, 53 | }, 54 | }, 55 | } 56 | 57 | // Create a new PeerConnection to the browser. 58 | peerConnection, err := webrtc.NewPeerConnection(config) 59 | if err != nil { 60 | panic(err) 61 | } 62 | defer peerConnection.Close() 63 | 64 | // Read incoming RTCP packets 65 | // Before these packets are returned they are processed by interceptors. For things 66 | // like NACK this needs to be called. 67 | processRTCP := func(rtpSender *webrtc.RTPSender) { 68 | rtcpBuf := make([]byte, 1500) 69 | for { 70 | if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { 71 | return 72 | } 73 | } 74 | } 75 | for _, rtpSender := range peerConnection.GetSenders() { 76 | go processRTCP(rtpSender) 77 | } 78 | 79 | // Wait for the offer to be pasted 80 | offer := webrtc.SessionDescription{} 81 | signal.Decode(signal.MustReadStdin(), &offer) 82 | 83 | if err = peerConnection.SetRemoteDescription(offer); err != nil { 84 | panic(err) 85 | } 86 | 87 | transcodedLocal, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "transcoded-track", "transcoded-h264-stream") 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | // Add the track to the PeerConnection 93 | rtpSender, err := peerConnection.AddTrack(transcodedLocal) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | go processRTCP(rtpSender) 99 | 100 | // Set a handler for when a new remote track starts 101 | peerConnection.OnTrack(func(trackRemote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 102 | fmt.Printf("on track %s (%s)\n", trackRemote.ID(), trackRemote.Codec().MimeType) 103 | 104 | // Create a new TrackLocal 105 | trackLocal, err := webrtc.NewTrackLocalStaticRTP(trackRemote.Codec().RTPCodecCapability, trackRemote.ID(), trackRemote.StreamID()) 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | // Read RTP packets being sent to Pion 111 | go func() { 112 | for { 113 | packet, _, readErr := trackRemote.ReadRTP() 114 | if readErr != nil { 115 | panic(readErr) 116 | } 117 | 118 | if writeErr := trackLocal.WriteRTP(packet); writeErr != nil && !errors.Is(writeErr, io.ErrClosedPipe) { 119 | panic(writeErr) 120 | } 121 | } 122 | }() 123 | 124 | transcodedRemote, err := tc.Transcode(trackLocal) 125 | if err != nil { 126 | panic(err) 127 | } 128 | 129 | // Write RTP packets back to the client. 130 | go func() { 131 | for { 132 | packet, _, readErr := transcodedRemote.ReadRTP() 133 | if readErr != nil { 134 | panic(readErr) 135 | } 136 | 137 | if writeErr := transcodedLocal.WriteRTP(packet); writeErr != nil && !errors.Is(writeErr, io.ErrClosedPipe) { 138 | panic(writeErr) 139 | } 140 | } 141 | }() 142 | }) 143 | 144 | // Set the handler for Peer connection state 145 | // This will notify you when the peer has connected/disconnected 146 | peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 147 | fmt.Printf("Peer Connection State has changed: %s\n", s.String()) 148 | 149 | if s == webrtc.PeerConnectionStateFailed { 150 | // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. 151 | // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. 152 | // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. 153 | fmt.Println("Peer Connection has gone to failed exiting") 154 | os.Exit(0) 155 | } 156 | }) 157 | 158 | // Create an answer 159 | answer, err := peerConnection.CreateAnswer(nil) 160 | if err != nil { 161 | panic(err) 162 | } 163 | 164 | // Create channel that is blocked until ICE Gathering is complete 165 | gatherComplete := webrtc.GatheringCompletePromise(peerConnection) 166 | 167 | // Sets the LocalDescription, and starts our UDP listeners 168 | err = peerConnection.SetLocalDescription(answer) 169 | if err != nil { 170 | panic(err) 171 | } 172 | 173 | // Block until ICE Gathering is complete, disabling trickle ICE 174 | // we do this because we only can exchange one signaling message 175 | // in a production application you should exchange ICE Candidates via OnICECandidate 176 | <-gatherComplete 177 | 178 | // Output the answer in base64 so we can paste it in browser 179 | fmt.Println(signal.Encode(*peerConnection.LocalDescription())) 180 | 181 | // Block forever 182 | select {} 183 | } 184 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muxable/transcoder 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/blendle/zapdriver v1.3.1 7 | github.com/mattn/go-pointer v0.0.1 8 | github.com/muxable/signal v0.0.0-20220222152720-780c4a7723b5 9 | github.com/pion/rtpio v0.1.4 10 | go.uber.org/goleak v1.1.12 11 | google.golang.org/grpc v1.43.0 12 | google.golang.org/protobuf v1.27.1 13 | ) 14 | 15 | require ( 16 | github.com/benbjohnson/clock v1.1.0 // indirect 17 | github.com/golang/protobuf v1.5.2 // indirect 18 | github.com/google/go-cmp v0.5.7 // indirect 19 | github.com/pkg/errors v0.9.1 // indirect 20 | go.uber.org/atomic v1.9.0 // indirect 21 | go.uber.org/multierr v1.7.0 // indirect 22 | golang.org/x/text v0.3.7 // indirect 23 | google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 // indirect 24 | ) 25 | 26 | require ( 27 | github.com/google/uuid v1.3.0 28 | github.com/pion/datachannel v1.5.2 // indirect 29 | github.com/pion/dtls/v2 v2.1.2 // indirect 30 | github.com/pion/ice/v2 v2.1.20 // indirect 31 | github.com/pion/interceptor v0.1.7 32 | github.com/pion/logging v0.2.2 // indirect 33 | github.com/pion/mdns v0.0.5 // indirect 34 | github.com/pion/randutil v0.1.0 // indirect 35 | github.com/pion/rtcp v1.2.9 // indirect 36 | github.com/pion/rtp v1.7.4 37 | github.com/pion/sctp v1.8.2 // indirect 38 | github.com/pion/sdp v1.3.0 39 | github.com/pion/sdp/v3 v3.0.4 40 | github.com/pion/srtp/v2 v2.0.5 // indirect 41 | github.com/pion/stun v0.3.5 // indirect 42 | github.com/pion/transport v0.13.0 // indirect 43 | github.com/pion/turn/v2 v2.0.6 // indirect 44 | github.com/pion/udp v0.1.1 // indirect 45 | github.com/pion/webrtc/v3 v3.1.23 46 | go.uber.org/zap v1.20.0 47 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect 48 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 49 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect 50 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 6 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 7 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 8 | github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= 9 | github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= 10 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 11 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 12 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 14 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 15 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 16 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 17 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 18 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 19 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 20 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 25 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 26 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 27 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 28 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= 29 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 30 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 31 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 32 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 33 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 34 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 35 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 36 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 37 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 38 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 40 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 41 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 42 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 43 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 44 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 45 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 46 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 47 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 48 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 49 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 50 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 51 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 52 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 53 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 58 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 59 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 60 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 61 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 62 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 63 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 64 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 65 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 66 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 67 | github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 68 | github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 69 | github.com/muxable/signal v0.0.0-20220222152720-780c4a7723b5 h1:aKfLFt7SNbze2Bd3xAJ59sKK5L7PgcBQqtxb5yTXQlM= 70 | github.com/muxable/signal v0.0.0-20220222152720-780c4a7723b5/go.mod h1:QTlYqYjGamoIxx8cHoL+I8NfLKT53WSxD78Jy0ZkBaE= 71 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 72 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 73 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 74 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 75 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 76 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 77 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 78 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 79 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 80 | github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E= 81 | github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= 82 | github.com/pion/dtls/v2 v2.1.1/go.mod h1:qG3gA7ZPZemBqpEFqRKyURYdKEwFZQCGb7gv9T3ON3Y= 83 | github.com/pion/dtls/v2 v2.1.2 h1:22Q1Jk9L++Yo7BIf9130MonNPfPVb+YgdYLeyQotuAA= 84 | github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= 85 | github.com/pion/ice/v2 v2.1.20 h1:xpxXyX5b4WjCh/D905gzBeW/hbJxMEPx2ptVfrhVE6M= 86 | github.com/pion/ice/v2 v2.1.20/go.mod h1:hEAldRzBhTtAfvlU1V/2/nLCMvveQWFKPNCop+63/Iw= 87 | github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo= 88 | github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U= 89 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 90 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 91 | github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= 92 | github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= 93 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 94 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 95 | github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= 96 | github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U= 97 | github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= 98 | github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 99 | github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA= 100 | github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 101 | github.com/pion/rtpio v0.1.4 h1:acaBSaJQknD7a3o1A9JqW6gG6GDcRzGq+VG7Yxtfk8E= 102 | github.com/pion/rtpio v0.1.4/go.mod h1:jMS8Vfs0XiGx4w0o/AbXEdY7pMNcgcIS11gPg0LxStE= 103 | github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= 104 | github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA= 105 | github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= 106 | github.com/pion/sdp v1.3.0 h1:21lpgEILHyolpsIrbCBagZaAPj4o057cFjzaFebkVOs= 107 | github.com/pion/sdp v1.3.0/go.mod h1:ceA2lTyftydQTuCIbUNoH77aAt6CiQJaRpssA4Gee8I= 108 | github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= 109 | github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= 110 | github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ= 111 | github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A= 112 | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= 113 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 114 | github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 115 | github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= 116 | github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY= 117 | github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= 118 | github.com/pion/turn/v2 v2.0.6 h1:AsXjSPR6Im15DMTB39NlfdTY9BQfieANPBjdg/aVNwY= 119 | github.com/pion/turn/v2 v2.0.6/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= 120 | github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= 121 | github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= 122 | github.com/pion/webrtc/v3 v3.1.23 h1:suyNiF9o2/6SBsyWA1UweraUWYkaHCNJdt/16b61I5w= 123 | github.com/pion/webrtc/v3 v3.1.23/go.mod h1:L5S/oAhL0Fzt/rnftVQRrP80/j5jygY7XRZzWwFx6P4= 124 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 125 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 126 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 127 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 128 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 129 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 130 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 131 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 132 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 135 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 136 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 137 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 138 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 139 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 140 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 141 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 142 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 143 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 144 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 145 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 146 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 147 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 148 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 149 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 150 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 151 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 152 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 153 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 154 | go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc= 155 | go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= 156 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 157 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 158 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 159 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 160 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= 161 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 162 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 163 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 164 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 165 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 166 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 167 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 168 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 169 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 170 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 171 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 172 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 173 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 174 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 175 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 176 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 177 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 178 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 179 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 180 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 181 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 182 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 183 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 184 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 185 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 186 | golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 187 | golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 188 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 189 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 190 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 191 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 192 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 194 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 195 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 197 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 199 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 200 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 201 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 212 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 213 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 214 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= 215 | golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 216 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 217 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 218 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 219 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 220 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 221 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 222 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 223 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 224 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 225 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 226 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 227 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 228 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 229 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 230 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 231 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 232 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 233 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 234 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 235 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 236 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 237 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 238 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 239 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 240 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 241 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 242 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 243 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 244 | google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q= 245 | google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= 246 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 247 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 248 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 249 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 250 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 251 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 252 | google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= 253 | google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= 254 | google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= 255 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 256 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 257 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 258 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 259 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 260 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 261 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 262 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 263 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 264 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 265 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 266 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 267 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 268 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 269 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 270 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 271 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 272 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 273 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 274 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 275 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 276 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 277 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 278 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 279 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 280 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 281 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 282 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 283 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 284 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 285 | -------------------------------------------------------------------------------- /pkg/av/avcodec.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavcodec 5 | #include 6 | */ 7 | import "C" 8 | import ( 9 | "github.com/pion/webrtc/v3" 10 | ) 11 | 12 | var AvCodec = map[string]uint32 { 13 | webrtc.MimeTypeVP8: C.AV_CODEC_ID_VP8, 14 | webrtc.MimeTypeVP9: C.AV_CODEC_ID_VP9, 15 | webrtc.MimeTypeH264: C.AV_CODEC_ID_H264, 16 | webrtc.MimeTypeH265: C.AV_CODEC_ID_HEVC, 17 | webrtc.MimeTypeG722: C.AV_CODEC_ID_ADPCM_G722, 18 | webrtc.MimeTypeOpus: C.AV_CODEC_ID_OPUS, 19 | webrtc.MimeTypePCMU: C.AV_CODEC_ID_PCM_MULAW, 20 | webrtc.MimeTypePCMA: C.AV_CODEC_ID_PCM_ALAW, 21 | } 22 | -------------------------------------------------------------------------------- /pkg/av/decode.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavcodec libavformat 5 | #include 6 | #include 7 | */ 8 | import "C" 9 | import ( 10 | "errors" 11 | "io" 12 | "strings" 13 | "unsafe" 14 | 15 | "github.com/pion/webrtc/v3" 16 | ) 17 | 18 | type DecodeContext struct { 19 | codec webrtc.RTPCodecParameters 20 | decoderctx *C.AVCodecContext 21 | pkt *AVPacket 22 | demuxer *DemuxContext 23 | } 24 | 25 | func NewDecoder(codec webrtc.RTPCodecParameters, demuxer *DemuxContext) *DecodeContext { 26 | return &DecodeContext{ 27 | codec: codec, 28 | pkt: NewAVPacket(), 29 | demuxer: demuxer, 30 | } 31 | } 32 | 33 | func (c *DecodeContext) init() error { 34 | if err := c.demuxer.init(); err != nil { 35 | return err 36 | } 37 | 38 | var decodercodec *C.AVCodec 39 | var kind int32 40 | if strings.HasPrefix(c.codec.MimeType, "video") { 41 | kind = C.AVMEDIA_TYPE_VIDEO 42 | } else if strings.HasPrefix(c.codec.MimeType, "audio") { 43 | kind = C.AVMEDIA_TYPE_AUDIO 44 | } else { 45 | kind = C.AVMEDIA_TYPE_UNKNOWN 46 | } 47 | 48 | ret := C.av_find_best_stream(c.demuxer.avformatctx, kind, -1, -1, &decodercodec, 0) 49 | if ret < 0 { 50 | return av_err("av_find_best_stream", ret) 51 | } 52 | 53 | decoderctx := C.avcodec_alloc_context3(decodercodec) 54 | if decoderctx == nil { 55 | return errors.New("failed to create decoder context") 56 | } 57 | 58 | if averr := C.avcodec_parameters_to_context(decoderctx, ((*[1 << 30]*C.AVStream)(unsafe.Pointer(c.demuxer.avformatctx.streams)))[ret].codecpar); averr < 0 { 59 | return av_err("avcodec_parameters_to_context", averr) 60 | } 61 | 62 | if averr := C.avcodec_open2(decoderctx, decodercodec, nil); averr < 0 { 63 | return av_err("avcodec_open2", averr) 64 | } 65 | 66 | c.decoderctx = decoderctx 67 | 68 | return nil 69 | } 70 | 71 | func (c *DecodeContext) ReadAVFrame(f *AVFrame) error { 72 | if res := C.avcodec_receive_frame(c.decoderctx, f.frame); res < 0 { 73 | if res == AVERROR(C.EAGAIN) { 74 | err := c.demuxer.ReadAVPacket(c.pkt) 75 | if err != nil && err != io.EOF { 76 | return err 77 | } 78 | 79 | if averr := C.avcodec_send_packet(c.decoderctx, c.pkt.packet); averr < 0 { 80 | return av_err("avcodec_send_packet", averr) 81 | } 82 | 83 | // try again. 84 | return c.ReadAVFrame(f) 85 | } 86 | C.avcodec_free_context(&c.decoderctx) 87 | if err := c.pkt.Close(); err != nil { 88 | return err 89 | } 90 | return av_err("failed to receive frame", res) 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/av/demux.c: -------------------------------------------------------------------------------- 1 | #include "demux.h" 2 | 3 | #include 4 | 5 | int cgoReadPacketFunc(void *opaque, uint8_t *buf, int buf_size) 6 | { 7 | return goReadPacketFunc(opaque, buf, buf_size); 8 | } 9 | 10 | int cgoWriteRTCPPacketFunc(void *opaque, uint8_t *buf, int buf_size) 11 | { 12 | return goWriteRTCPPacketFunc(opaque, buf, buf_size); 13 | } -------------------------------------------------------------------------------- /pkg/av/demux.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavformat 5 | #include 6 | #include "demux.h" 7 | */ 8 | import "C" 9 | import ( 10 | "errors" 11 | "io" 12 | "os" 13 | "unsafe" 14 | 15 | "github.com/mattn/go-pointer" 16 | "github.com/pion/rtpio/pkg/rtpio" 17 | "github.com/pion/webrtc/v3" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | type DemuxContext struct { 22 | codec webrtc.RTPCodecParameters 23 | avformatctx *C.AVFormatContext 24 | in rtpio.RTPReader 25 | sdpfile *os.File 26 | } 27 | 28 | var ( 29 | csdpflags = C.CString("sdp_flags") 30 | ccustomio = C.CString("custom_io") 31 | creorderqueuesize = C.CString("reorder_queue_size") 32 | ) 33 | 34 | func NewDemuxer(codec webrtc.RTPCodecParameters, in rtpio.RTPReader) *DemuxContext { 35 | return &DemuxContext{ 36 | codec: codec, 37 | in: in, 38 | } 39 | } 40 | 41 | //export goReadPacketFunc 42 | func goReadPacketFunc(opaque unsafe.Pointer, buf *C.uint8_t, bufsize C.int) C.int { 43 | d := pointer.Restore(opaque).(*DemuxContext) 44 | p, err := d.in.ReadRTP() 45 | if err != nil { 46 | if err == io.EOF { 47 | d.sdpfile.Close() 48 | os.Remove(d.sdpfile.Name()) 49 | } else { 50 | zap.L().Error("failed to read RTP packet", zap.Error(err)) 51 | } 52 | return C.int(-1) 53 | } 54 | 55 | b, err := p.Marshal() 56 | if err != nil { 57 | zap.L().Error("failed to marshal RTP packet", zap.Error(err)) 58 | return C.int(-1) 59 | } 60 | 61 | if C.int(len(b)) > bufsize { 62 | zap.L().Error("RTP packet too large", zap.Int("size", len(b))) 63 | return C.int(-1) 64 | } 65 | 66 | C.memcpy(unsafe.Pointer(buf), unsafe.Pointer(&b[0]), C.ulong(len(b))) 67 | 68 | return C.int(len(b)) 69 | } 70 | 71 | 72 | //export goWriteRTCPPacketFunc 73 | func goWriteRTCPPacketFunc(opaque unsafe.Pointer, buf *C.uint8_t, bufsize C.int) C.int { 74 | // this function is necessary: https://trac.ffmpeg.org/ticket/9670 75 | return bufsize 76 | } 77 | 78 | func (c *DemuxContext) init() error { 79 | avformatctx := C.avformat_alloc_context() 80 | if avformatctx == nil { 81 | return errors.New("failed to create format context") 82 | } 83 | 84 | var opts *C.AVDictionary 85 | defer C.av_dict_free(&opts) 86 | if averr := C.av_dict_set(&opts, csdpflags, ccustomio, 0); averr < 0 { 87 | return av_err("av_dict_set", averr) 88 | } 89 | if averr := C.av_dict_set_int(&opts, creorderqueuesize, C.int64_t(0), 0); averr < 0 { 90 | return av_err("av_dict_set", averr) 91 | } 92 | 93 | sdpfile, err := NewTempSDP(c.codec) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | cfilename := C.CString(sdpfile.Name()) 99 | defer C.free(unsafe.Pointer(cfilename)) 100 | 101 | if averr := C.avformat_open_input(&avformatctx, cfilename, nil, &opts); averr < C.int(0) { 102 | return av_err("avformat_open_input", averr) 103 | } 104 | 105 | buf := C.av_malloc(1500) 106 | if buf == nil { 107 | return errors.New("failed to allocate buffer") 108 | } 109 | 110 | avioctx := C.avio_alloc_context((*C.uchar)(buf), 1500, 1, pointer.Save(c), (*[0]byte)(C.cgoReadPacketFunc), (*[0]byte)(C.cgoWriteRTCPPacketFunc), nil) 111 | if avioctx == nil { 112 | return errors.New("failed to allocate avio context") 113 | } 114 | 115 | avformatctx.pb = avioctx 116 | 117 | if averr := C.avformat_find_stream_info(avformatctx, nil); averr < C.int(0) { 118 | return av_err("avformat_find_stream_info", averr) 119 | } 120 | 121 | c.avformatctx = avformatctx 122 | c.sdpfile = sdpfile 123 | 124 | return nil 125 | } 126 | 127 | func (c *DemuxContext) ReadAVPacket(p *AVPacket) error { 128 | averr := C.av_read_frame(c.avformatctx, p.packet) 129 | if averr < 0 { 130 | err := av_err("av_read_frame", averr) 131 | if err == io.EOF { 132 | // TODO: is this necessary? does ffmpeg do it automatically? 133 | p.packet = nil 134 | } 135 | C.avformat_free_context(c.avformatctx) 136 | if err := c.sdpfile.Close(); err != nil { 137 | return err 138 | } 139 | return err 140 | } 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /pkg/av/demux.h: -------------------------------------------------------------------------------- 1 | #ifndef DEMUX_H 2 | #define DEMUX_H 3 | 4 | #include 5 | 6 | extern int goReadPacketFunc(void *, uint8_t *, int); 7 | extern int goWriteRTCPPacketFunc(void *, uint8_t *, int); 8 | 9 | int cgoReadPacketFunc(void *, uint8_t *, int); 10 | int cgoWriteRTCPPacketFunc(void *, uint8_t *, int); 11 | 12 | #endif -------------------------------------------------------------------------------- /pkg/av/encode.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavcodec libavformat 5 | #include 6 | #include 7 | */ 8 | import "C" 9 | import ( 10 | "errors" 11 | "io" 12 | 13 | "github.com/pion/webrtc/v3" 14 | ) 15 | 16 | type EncodeContext struct { 17 | codec webrtc.RTPCodecCapability 18 | encoderctx *C.AVCodecContext 19 | frame *AVFrame 20 | decoder *DecodeContext 21 | } 22 | 23 | func NewEncoder(codec webrtc.RTPCodecCapability, decoder *DecodeContext) *EncodeContext { 24 | return &EncodeContext{ 25 | codec: codec, 26 | frame: NewAVFrame(), 27 | decoder: decoder, 28 | } 29 | } 30 | 31 | func (c *EncodeContext) init() error { 32 | if err := c.decoder.init(); err != nil { 33 | return err 34 | } 35 | 36 | decoderctx := c.decoder.decoderctx 37 | 38 | encodercodec := C.avcodec_find_encoder(AvCodec[c.codec.MimeType]) 39 | if encodercodec == nil { 40 | return errors.New("failed to start encoder") 41 | } 42 | 43 | encoderctx := C.avcodec_alloc_context3(encodercodec) 44 | if encoderctx == nil { 45 | return errors.New("failed to create encoder context") 46 | } 47 | 48 | encoderctx.channels = decoderctx.channels 49 | encoderctx.channel_layout = decoderctx.channel_layout 50 | encoderctx.sample_rate = C.int(c.codec.ClockRate) 51 | encoderctx.sample_fmt = decoderctx.sample_fmt 52 | encoderctx.width = decoderctx.width 53 | encoderctx.height = decoderctx.height 54 | encoderctx.pix_fmt = C.AV_PIX_FMT_YUV420P 55 | encoderctx.time_base = C.av_make_q(C.int(1), C.int(c.codec.ClockRate)) 56 | 57 | var opts *C.AVDictionary 58 | defer C.av_dict_free(&opts) 59 | 60 | if c.codec.MimeType == webrtc.MimeTypeH264 { 61 | if averr := C.av_dict_set(&opts, C.CString("preset"), C.CString("ultrafast"), 0); averr < 0 { 62 | return av_err("av_dict_set", averr) 63 | } 64 | if averr := C.av_dict_set(&opts, C.CString("tune"), C.CString("zerolatency"), 0); averr < 0 { 65 | return av_err("av_dict_set", averr) 66 | } 67 | if averr := C.av_dict_set(&opts, C.CString("profile"), C.CString("baseline"), 0); averr < 0 { 68 | return av_err("av_dict_set", averr) 69 | } 70 | } 71 | 72 | if averr := C.avcodec_open2(encoderctx, encodercodec, &opts); averr < 0 { 73 | return av_err("avcodec_open2", averr) 74 | } 75 | 76 | encoderctx.rc_buffer_size = 4 * 1000 * 1000 77 | encoderctx.rc_max_rate = 20 * 1000 * 1000 78 | encoderctx.rc_min_rate = 1 * 1000 * 1000 79 | 80 | c.encoderctx = encoderctx 81 | 82 | return nil 83 | } 84 | 85 | func (c *EncodeContext) ReadAVPacket(p *AVPacket) error { 86 | if res := C.avcodec_receive_packet(c.encoderctx, p.packet); res < 0 { 87 | if res == AVERROR(C.EAGAIN) { 88 | err := c.decoder.ReadAVFrame(c.frame) 89 | if err != nil && err != io.EOF { 90 | return err 91 | } 92 | 93 | if c.frame.frame.pts != C.AV_NOPTS_VALUE { 94 | if res := C.avcodec_send_frame(c.encoderctx, c.frame.frame); res < 0 { 95 | return av_err("avcodec_send_frame", res) 96 | } 97 | } 98 | 99 | // try again. 100 | return c.ReadAVPacket(p) 101 | } 102 | C.avcodec_free_context(&c.encoderctx) 103 | if err := c.frame.Close(); err != nil { 104 | return err 105 | } 106 | return av_err("avcodec_receive_packet", res) 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /pkg/av/error.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavutil 5 | #include 6 | */ 7 | import "C" 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "io" 12 | "unsafe" 13 | ) 14 | 15 | func AVERROR(code C.int) C.int { 16 | return -code 17 | } 18 | 19 | func av_err(prefix string, averr C.int) error { 20 | if averr == -541478725 { // special error code. 21 | return io.EOF 22 | } 23 | errlen := 1024 24 | b := make([]byte, errlen) 25 | C.av_strerror(averr, (*C.char)(unsafe.Pointer(&b[0])), C.size_t(errlen)) 26 | return fmt.Errorf("%s: %s (%d)", prefix, string(b[:bytes.Index(b, []byte{0})]), averr) 27 | } -------------------------------------------------------------------------------- /pkg/av/mux.c: -------------------------------------------------------------------------------- 1 | #include "mux.h" 2 | 3 | #include 4 | 5 | int cgoWritePacketFunc(void *opaque, uint8_t *buf, int buf_size) 6 | { 7 | return goWritePacketFunc(opaque, buf, buf_size); 8 | } -------------------------------------------------------------------------------- /pkg/av/mux.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavcodec libavformat 5 | #include 6 | #include 7 | #include "mux.h" 8 | */ 9 | import "C" 10 | import ( 11 | "errors" 12 | "io" 13 | "unsafe" 14 | 15 | "github.com/mattn/go-pointer" 16 | "github.com/pion/rtp" 17 | "github.com/pion/webrtc/v3" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | var ( 22 | crtp = C.CString("rtp") 23 | ) 24 | 25 | type result struct { 26 | p *rtp.Packet 27 | e error 28 | } 29 | 30 | type MuxContext struct { 31 | codec webrtc.RTPCodecCapability 32 | avformatctx *C.AVFormatContext 33 | packet *AVPacket 34 | encoder *EncodeContext 35 | pch chan *result 36 | } 37 | 38 | func NewMuxer(codec webrtc.RTPCodecCapability, encoder *EncodeContext) *MuxContext { 39 | c := &MuxContext{ 40 | codec: codec, 41 | packet: NewAVPacket(), 42 | encoder: encoder, 43 | pch: make(chan *result), 44 | } 45 | go func() { 46 | defer close(c.pch) 47 | defer C.av_write_trailer(c.avformatctx) 48 | if err := c.init(); err != nil { 49 | c.pch <- &result{e: err} 50 | return 51 | } 52 | for { 53 | if err := c.encoder.ReadAVPacket(c.packet); err != nil { 54 | c.pch <- &result{e: err} 55 | close(c.pch) 56 | return 57 | } 58 | if averr := C.av_write_frame(c.avformatctx, c.packet.packet); averr < 0 { 59 | c.pch <- &result{e: av_err("av_write_frame", averr)} 60 | return 61 | } 62 | } 63 | }() 64 | return c 65 | } 66 | 67 | //export goWritePacketFunc 68 | func goWritePacketFunc(opaque unsafe.Pointer, buf *C.uint8_t, bufsize C.int) C.int { 69 | m := pointer.Restore(opaque).(*MuxContext) 70 | b := C.GoBytes(unsafe.Pointer(buf), bufsize) 71 | p := &rtp.Packet{} 72 | if err := p.Unmarshal(b); err != nil { 73 | zap.L().Error("failed to unmarshal rtp packet", zap.Error(err)) 74 | return C.int(-1) 75 | } 76 | m.pch <- &result{p: p} 77 | return bufsize 78 | } 79 | 80 | func (c *MuxContext) init() error { 81 | if err := c.encoder.init(); err != nil { 82 | return err 83 | } 84 | 85 | outputformat := C.av_guess_format(crtp, nil, nil) 86 | if outputformat == nil { 87 | return errors.New("failed to find rtp output format") 88 | } 89 | 90 | buf := C.av_malloc(1200) 91 | if buf == nil { 92 | return errors.New("failed to allocate buffer") 93 | } 94 | 95 | var avformatctx *C.AVFormatContext 96 | 97 | if averr := C.avformat_alloc_output_context2(&avformatctx, outputformat, nil, nil); averr < 0 { 98 | return av_err("avformat_alloc_output_context2", averr) 99 | } 100 | 101 | avioctx := C.avio_alloc_context((*C.uchar)(buf), 1200, 1, pointer.Save(c), nil, (*[0]byte)(C.cgoWritePacketFunc), nil) 102 | if avioctx == nil { 103 | return errors.New("failed to create avio context") 104 | } 105 | 106 | avioctx.max_packet_size = 1200 107 | 108 | avformatctx.pb = avioctx 109 | 110 | avformatstream := C.avformat_new_stream(avformatctx, c.encoder.encoderctx.codec) 111 | if avformatstream == nil { 112 | return errors.New("failed to create rtp stream") 113 | } 114 | 115 | if averr := C.avcodec_parameters_from_context(avformatstream.codecpar, c.encoder.encoderctx); averr < 0 { 116 | return av_err("avcodec_parameters_from_context", averr) 117 | } 118 | 119 | if averr := C.avformat_write_header(avformatctx, nil); averr < 0 { 120 | return av_err("avformat_write_header", averr) 121 | } 122 | 123 | c.avformatctx = avformatctx 124 | 125 | return nil 126 | } 127 | 128 | func (c *MuxContext) ReadRTP() (*rtp.Packet, error) { 129 | r, ok := <-c.pch 130 | if !ok { 131 | return nil, io.EOF 132 | } 133 | return r.p, r.e 134 | } 135 | -------------------------------------------------------------------------------- /pkg/av/mux.h: -------------------------------------------------------------------------------- 1 | #ifndef MUX_H 2 | #define MUX_H 3 | 4 | #include 5 | 6 | extern int goWritePacketFunc(void *, uint8_t *, int); 7 | 8 | int cgoWritePacketFunc(void *, uint8_t *, int); 9 | 10 | #endif -------------------------------------------------------------------------------- /pkg/av/temp_sdp.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/pion/sdp" 12 | "github.com/pion/webrtc/v3" 13 | ) 14 | 15 | func kind(codec webrtc.RTPCodecParameters) string { 16 | if strings.HasPrefix(codec.MimeType, "video") { 17 | return "video" 18 | } else if strings.HasPrefix(codec.MimeType, "audio") { 19 | return "audio" 20 | } 21 | return "" 22 | } 23 | 24 | func NewTempSDP(codec webrtc.RTPCodecParameters) (*os.File, error) { 25 | s := &sdp.SessionDescription{ 26 | Version: 0, 27 | Origin: sdp.Origin{ 28 | Username: "-", 29 | NetworkType: "IN", 30 | AddressType: "IP4", 31 | UnicastAddress: "127.0.0.1", 32 | }, 33 | SessionName: "Pion WebRTC", 34 | } 35 | 36 | pt := strconv.FormatUint(uint64(codec.PayloadType), 10) 37 | rtpmap := fmt.Sprintf("%s/%d", strings.Split(codec.MimeType, "/")[1], codec.ClockRate) 38 | if codec.Channels > 0 { 39 | rtpmap += fmt.Sprintf("/%d", codec.Channels) 40 | } 41 | fmtp := codec.SDPFmtpLine 42 | s.WithMedia(&sdp.MediaDescription{ 43 | ConnectionInformation: &sdp.ConnectionInformation{ 44 | NetworkType: "IN", 45 | AddressType: "IP4", 46 | Address: &sdp.Address{IP: net.IPv4(127, 0, 0, 1)}, 47 | }, 48 | MediaName: sdp.MediaName{ 49 | Media: kind(codec), 50 | Port: sdp.RangedPort{Value: 6000, Range: nil}, // this value doesn't actually matter. 51 | Protos: []string{"RTP", "AVP"}, 52 | Formats: []string{pt}, 53 | }, 54 | Attributes: []sdp.Attribute{ 55 | { 56 | Key: "rtpmap", 57 | Value: fmt.Sprintf("%s %s", pt, rtpmap), 58 | }, 59 | { 60 | Key: "fmtp", 61 | Value: fmt.Sprintf("%s %s", pt, fmtp), 62 | }, 63 | }, 64 | }) 65 | file, err := ioutil.TempFile("", "pion-webrtc-sdp") 66 | if err != nil { 67 | return nil, err 68 | } 69 | if _, err := file.WriteString(s.Marshal()); err != nil { 70 | return nil, err 71 | } 72 | return file, nil 73 | } -------------------------------------------------------------------------------- /pkg/av/transcoder.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavutil 5 | #include 6 | */ 7 | import "C" 8 | import ( 9 | "github.com/pion/rtpio/pkg/rtpio" 10 | "github.com/pion/webrtc/v3" 11 | ) 12 | 13 | func init() { 14 | C.av_log_set_level(48) 15 | } 16 | 17 | type Transcoder struct { 18 | rtpio.RTPWriteCloser 19 | rtpio.RTPReader 20 | } 21 | 22 | func NewTranscoder(from webrtc.RTPCodecParameters, to webrtc.RTPCodecCapability) (*Transcoder, error) { 23 | r, w := rtpio.RTPPipe() 24 | demux := NewDemuxer(from, r) 25 | decode := NewDecoder(from, demux) 26 | encode := NewEncoder(to, decode) 27 | mux := NewMuxer(to, encode) 28 | 29 | return &Transcoder{ 30 | RTPWriteCloser: w, 31 | RTPReader: mux, 32 | }, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/av/types.go: -------------------------------------------------------------------------------- 1 | package av 2 | 3 | /* 4 | #cgo pkg-config: libavcodec 5 | #include 6 | */ 7 | import "C" 8 | 9 | // These are useful to avoid leaking the cgo interface. 10 | 11 | type AVPacket struct { 12 | packet *C.AVPacket 13 | } 14 | 15 | func NewAVPacket() *AVPacket { 16 | packet := C.av_packet_alloc() 17 | if packet == nil { 18 | return nil 19 | } 20 | return &AVPacket{packet: packet} 21 | } 22 | 23 | func (p *AVPacket) Close() error { 24 | C.av_packet_free(&p.packet) 25 | return nil 26 | } 27 | 28 | type AVFrame struct { 29 | frame *C.AVFrame 30 | } 31 | 32 | func NewAVFrame() *AVFrame { 33 | frame := C.av_frame_alloc() 34 | if frame == nil { 35 | return nil 36 | } 37 | return &AVFrame{frame: frame} 38 | } 39 | 40 | func (f *AVFrame) Close() error { 41 | C.av_frame_free(&f.frame) 42 | return nil 43 | } -------------------------------------------------------------------------------- /pkg/codecs/codecs.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "github.com/pion/webrtc/v3" 5 | ) 6 | 7 | // mirroring https://chromium.googlesource.com/external/webrtc/+/95eb1ba0db79d8fd134ae61b0a24648598684e8a/webrtc/media/engine/payload_type_mapper.cc#27 8 | // 9 | // TODO: a better approach to this would be to list all supported codecs (including duplicates) and let WebRTC negotiate the codec 10 | // then pass that to the transcoder. we will need to implement our own TrackLocal for this. 11 | var DefaultOutputCodecs = map[string]webrtc.RTPCodecParameters{ 12 | webrtc.MimeTypePCMU: { 13 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypePCMU, ClockRate: 8000, Channels: 1}, 14 | PayloadType: 0, 15 | }, 16 | "audio/GSM": { 17 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/GSM", ClockRate: 8000, Channels: 1}, 18 | PayloadType: 3, 19 | }, 20 | "audio/G723": { 21 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/G723", ClockRate: 8000, Channels: 1}, 22 | PayloadType: 4, 23 | }, 24 | "audio/LPC": { 25 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/LPC", ClockRate: 8000, Channels: 1}, 26 | PayloadType: 7, 27 | }, 28 | webrtc.MimeTypePCMA: { 29 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypePCMA, ClockRate: 8000, Channels: 1}, 30 | PayloadType: 8, 31 | }, 32 | webrtc.MimeTypeG722: { 33 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeG722, ClockRate: 8000, Channels: 1}, 34 | PayloadType: 9, 35 | }, 36 | "audio/L16": { 37 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/L16", ClockRate: 44100, Channels: 2}, 38 | PayloadType: 10, 39 | }, 40 | "audio/QCELP": { 41 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/QCELP", ClockRate: 8000, Channels: 1}, 42 | PayloadType: 12, 43 | }, 44 | "audio/CN": { 45 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/CN", ClockRate: 8000, Channels: 1}, 46 | PayloadType: 13, 47 | }, 48 | "audio/MPA": { 49 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/MPA", ClockRate: 90000, Channels: 1}, 50 | PayloadType: 14, 51 | }, 52 | "audio/G728": { 53 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/G728", ClockRate: 8000, Channels: 1}, 54 | PayloadType: 15, 55 | }, 56 | "audio/DVI4": { 57 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/DVI4", ClockRate: 22050, Channels: 1}, 58 | PayloadType: 17, 59 | }, 60 | "audio/G729": { 61 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/G729", ClockRate: 8000, Channels: 1}, 62 | PayloadType: 18, 63 | }, 64 | 65 | webrtc.MimeTypeVP8: { 66 | RTPCodecCapability: webrtc.RTPCodecCapability{ 67 | MimeType: webrtc.MimeTypeVP8, 68 | ClockRate: 90000, 69 | RTCPFeedback: []webrtc.RTCPFeedback{{Type: "goog-remb", Parameter: ""}, {Type: "ccm", Parameter: "fir"}, {Type: "nack", Parameter: ""}, {Type: "nack", Parameter: "pli"}}, 70 | }, 71 | PayloadType: 100, 72 | }, 73 | webrtc.MimeTypeVP9: { 74 | RTPCodecCapability: webrtc.RTPCodecCapability{ 75 | MimeType: webrtc.MimeTypeVP9, 76 | ClockRate: 90000, 77 | RTCPFeedback: []webrtc.RTCPFeedback{{Type: "goog-remb", Parameter: ""}, {Type: "ccm", Parameter: "fir"}, {Type: "nack", Parameter: ""}, {Type: "nack", Parameter: "pli"}}, 78 | }, 79 | PayloadType: 101, 80 | }, 81 | webrtc.MimeTypeH264: { 82 | RTPCodecCapability: webrtc.RTPCodecCapability{ 83 | MimeType: webrtc.MimeTypeH264, 84 | ClockRate: 90000, 85 | RTCPFeedback: []webrtc.RTCPFeedback{{Type: "goog-remb", Parameter: ""}, {Type: "ccm", Parameter: "fir"}, {Type: "nack", Parameter: ""}, {Type: "nack", Parameter: "pli"}}, 86 | }, 87 | PayloadType: 102, 88 | }, 89 | webrtc.MimeTypeH265: { 90 | RTPCodecCapability: webrtc.RTPCodecCapability{ 91 | MimeType: webrtc.MimeTypeH265, 92 | ClockRate: 90000, 93 | RTCPFeedback: []webrtc.RTCPFeedback{{Type: "goog-remb", Parameter: ""}, {Type: "ccm", Parameter: "fir"}, {Type: "nack", Parameter: ""}, {Type: "nack", Parameter: "pli"}}, 94 | }, 95 | PayloadType: 103, 96 | }, 97 | webrtc.MimeTypeAV1: { 98 | RTPCodecCapability: webrtc.RTPCodecCapability{ 99 | MimeType: webrtc.MimeTypeAV1, 100 | ClockRate: 90000, 101 | RTCPFeedback: []webrtc.RTCPFeedback{{Type: "goog-remb", Parameter: ""}, {Type: "ccm", Parameter: "fir"}, {Type: "nack", Parameter: ""}, {Type: "nack", Parameter: "pli"}}, 102 | }, 103 | PayloadType: 104, 104 | }, 105 | 106 | webrtc.MimeTypeOpus: { 107 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: "minptime=10;useinbandfec=1"}, 108 | PayloadType: 111, 109 | }, 110 | "audio/AC3": { 111 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/AC3", ClockRate: 48000, Channels: 1}, 112 | PayloadType: 112, 113 | }, 114 | "audio/VORBIS": { 115 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/VORBIS", ClockRate: 90000, Channels: 1}, 116 | PayloadType: 113, 117 | }, 118 | "audio/AAC": { 119 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/AAC", ClockRate: 48000, Channels: 2}, 120 | PayloadType: 114, 121 | }, 122 | "audio/SPEEX": { 123 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/SPEEX", ClockRate: 48000, Channels: 1}, 124 | PayloadType: 115, 125 | }, 126 | } 127 | 128 | type GStreamerParameters struct { 129 | DefaultEncoder, Payloader string 130 | } 131 | 132 | var SupportedCodecs = map[string]GStreamerParameters{ 133 | webrtc.MimeTypeH264: { 134 | "video/x-raw,format=I420 ! x264enc pass=5 quantizer=25 speed-preset=fast key-int-max=20 byte-stream=true", "rtph264pay pt=102 config-interval=-1", 135 | }, 136 | webrtc.MimeTypeH265: { 137 | "x265enc speed-preset=ultrafast tune=zerolatency key-int-max=20", "rtph265pay", 138 | }, 139 | webrtc.MimeTypeVP8: { 140 | "vp8enc end-usage=cq error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5", "rtpvp8pay", 141 | }, 142 | webrtc.MimeTypeVP9: { 143 | "vp9enc end-usage=cq error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5", "rtpvp9pay", 144 | }, 145 | // webrtc.MimeTypeAV1: { 146 | // "rtpav1depay", "av1enc deadline=1", "rtpav1pay", 147 | // func(c webrtc.RTPCodecParameters) string { 148 | // return fmt.Sprintf("encoding-name=AV1", c.ClockRate, c.PayloadType) 149 | // }, 150 | // }, 151 | 152 | webrtc.MimeTypeOpus: {"opusenc inband-fec=true", "rtpopuspay pt=111"}, 153 | "audio/AAC": {"avenc_aac", "rtpmp4apay"}, 154 | "audio/SPEEX": {"speexenc", "rtpspeexpay"}, 155 | webrtc.MimeTypeG722: {"avenc_g722", "rtpg722pay"}, 156 | webrtc.MimeTypePCMA: {"alawenc", "rtppcmapay"}, 157 | webrtc.MimeTypePCMU: {"mulawenc", "rtppcmupay"}, 158 | "audio/AC3": {"avenc_ac3", "rtpac3pay"}, 159 | } 160 | -------------------------------------------------------------------------------- /pkg/codecs/packetizer.go: -------------------------------------------------------------------------------- 1 | package codecs 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/pion/rtp" 8 | ) 9 | 10 | type packetizer struct { 11 | MTU uint16 12 | Payloader rtp.Payloader 13 | Sequencer rtp.Sequencer 14 | TimestampOffset uint32 15 | extensionNumbers struct { // put extension numbers in here. If they're 0, the extension is disabled (0 is not a legal extension number) 16 | AbsSendTime int // http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 17 | } 18 | timegen func() time.Time 19 | } 20 | 21 | // NewTSPacketizer returns a new instance of a Packetizer for a specific payloader 22 | func NewTSPacketizer(mtu uint16, payloader rtp.Payloader, sequencer rtp.Sequencer) rtp.Packetizer { 23 | src := rand.NewSource(time.Now().UnixNano()) 24 | return &packetizer{ 25 | MTU: mtu, 26 | Payloader: payloader, 27 | Sequencer: sequencer, 28 | TimestampOffset: uint32(src.Int63()), 29 | timegen: time.Now, 30 | } 31 | } 32 | 33 | func (p *packetizer) EnableAbsSendTime(value int) { 34 | p.extensionNumbers.AbsSendTime = value 35 | } 36 | 37 | // Packetize packetizes the payload of an RTP packet and returns one or more RTP packets 38 | func (p *packetizer) Packetize(payload []byte, pts uint32) []*rtp.Packet { 39 | // Guard against an empty payload 40 | if len(payload) == 0 { 41 | return nil 42 | } 43 | 44 | payloads := p.Payloader.Payload(p.MTU-12, payload) 45 | packets := make([]*rtp.Packet, len(payloads)) 46 | 47 | for i, pp := range payloads { 48 | packets[i] = &rtp.Packet{ 49 | Header: rtp.Header{ 50 | Version: 2, 51 | Padding: false, 52 | Extension: false, 53 | Marker: i == len(payloads)-1, 54 | SequenceNumber: p.Sequencer.NextSequenceNumber(), 55 | Timestamp: pts + p.TimestampOffset, 56 | }, 57 | Payload: pp, 58 | } 59 | } 60 | 61 | if len(packets) != 0 && p.extensionNumbers.AbsSendTime != 0 { 62 | sendTime := rtp.NewAbsSendTimeExtension(p.timegen()) 63 | // apply http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 64 | b, err := sendTime.Marshal() 65 | if err != nil { 66 | return nil // never happens 67 | } 68 | err = packets[len(packets)-1].SetExtension(uint8(p.extensionNumbers.AbsSendTime), b) 69 | if err != nil { 70 | return nil // never happens 71 | } 72 | } 73 | 74 | return packets 75 | } 76 | 77 | // SkipSamples causes a gap in sample count between Packetize requests so the 78 | // RTP payloads produced have a gap in timestamps 79 | func (p *packetizer) SkipSamples(skippedSamples uint32) { 80 | } 81 | -------------------------------------------------------------------------------- /pkg/transcoder/client.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/muxable/signal/pkg/signal" 8 | "github.com/muxable/transcoder/api" 9 | "github.com/pion/interceptor" 10 | "github.com/pion/webrtc/v3" 11 | "go.uber.org/zap" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | type Client struct { 16 | ctx context.Context 17 | conn *grpc.ClientConn 18 | } 19 | 20 | func NewClient(ctx context.Context, conn *grpc.ClientConn) (*Client, error) { 21 | return &Client{ 22 | ctx: ctx, 23 | conn: conn, 24 | }, nil 25 | } 26 | 27 | type TranscodeOption func(*api.TranscodeRequest) 28 | 29 | func (c *Client) Transcode(tl *webrtc.TrackLocalStaticRTP, options ...TranscodeOption) (*webrtc.TrackRemote, error) { 30 | config := webrtc.Configuration{ 31 | ICEServers: []webrtc.ICEServer{ 32 | {URLs: []string{"stun:stun.l.google.com:19302"}}, 33 | }, 34 | } 35 | 36 | m := &webrtc.MediaEngine{} 37 | 38 | if err := m.RegisterCodec(webrtc.RTPCodecParameters{RTPCodecCapability: tl.Codec(), PayloadType: webrtc.PayloadType(96)}, tl.Kind()); err != nil { 39 | return nil, err 40 | } 41 | 42 | i := &interceptor.Registry{} 43 | if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { 44 | return nil, err 45 | } 46 | 47 | send, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)).NewPeerConnection(config) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | sendSignaller := signal.Negotiate(send) 53 | 54 | sendClient := api.NewTranscoderClient(c.conn) 55 | 56 | sendSignal, err := sendClient.Publish(c.ctx) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | go func() { 62 | for { 63 | signal, err := sendSignaller.ReadSignal() 64 | if err != nil { 65 | zap.L().Error("failed to read signal", zap.Error(err)) 66 | return 67 | } 68 | 69 | log.Printf("signal %v", signal) 70 | 71 | if err := sendSignal.Send(signal); err != nil { 72 | zap.L().Error("failed to send signal", zap.Error(err)) 73 | return 74 | } 75 | } 76 | }() 77 | 78 | go func() { 79 | defer send.Close() 80 | for { 81 | in, err := sendSignal.Recv() 82 | if err != nil { 83 | zap.L().Error("failed to receive", zap.Error(err)) 84 | return 85 | } 86 | log.Printf("signal %v", in) 87 | 88 | if err := sendSignaller.WriteSignal(in); err != nil { 89 | zap.L().Error("failed to write signal", zap.Error(err)) 90 | return 91 | } 92 | } 93 | }() 94 | 95 | rtpSender, err := send.AddTrack(tl) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | go func() { 101 | buf := make([]byte, 1500) 102 | for { 103 | if _, _, err := rtpSender.Read(buf); err != nil { 104 | return 105 | } 106 | } 107 | }() 108 | 109 | recv, err := webrtc.NewPeerConnection(config) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | recvSignaller := signal.Negotiate(recv) 115 | 116 | recvClient := api.NewTranscoderClient(c.conn) 117 | 118 | recvSignal, err := recvClient.Subscribe(c.ctx) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | promise := make(chan *webrtc.TrackRemote) 124 | 125 | recv.OnTrack(func(tr *webrtc.TrackRemote, r *webrtc.RTPReceiver) { 126 | go func() { 127 | buf := make([]byte, 1500) 128 | for { 129 | if _, _, err := r.Read(buf); err != nil { 130 | return 131 | } 132 | } 133 | }() 134 | 135 | log.Printf("got track %v", tr) 136 | 137 | promise <- tr 138 | }) 139 | 140 | go func() { 141 | for { 142 | signal, err := recvSignaller.ReadSignal() 143 | if err != nil { 144 | zap.L().Error("failed to read signal", zap.Error(err)) 145 | return 146 | } 147 | 148 | if err := recvSignal.Send(&api.SubscribeRequest{Operation: &api.SubscribeRequest_Signal{Signal: signal}}); err != nil { 149 | zap.L().Error("failed to send signal", zap.Error(err)) 150 | return 151 | } 152 | } 153 | }() 154 | 155 | go func() { 156 | defer recv.Close() 157 | for { 158 | in, err := recvSignal.Recv() 159 | if err != nil { 160 | zap.L().Error("failed to receive", zap.Error(err)) 161 | return 162 | } 163 | 164 | if err := recvSignaller.WriteSignal(in); err != nil { 165 | zap.L().Error("failed to write signal", zap.Error(err)) 166 | return 167 | } 168 | } 169 | }() 170 | 171 | request := &api.TranscodeRequest{ 172 | StreamId: tl.StreamID(), 173 | TrackId: tl.ID(), 174 | RtpStreamId: tl.RID(), 175 | } 176 | 177 | for _, option := range options { 178 | option(request) 179 | } 180 | 181 | if err := recvSignal.Send(&api.SubscribeRequest{Operation: &api.SubscribeRequest_Request{Request: request}}); err != nil { 182 | return nil, err 183 | } 184 | 185 | return <-promise, nil 186 | } 187 | 188 | func ToMimeType(mimeType string) TranscodeOption { 189 | return func(request *api.TranscodeRequest) { 190 | request.MimeType = mimeType 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /pkg/transcoder/server.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/muxable/signal/pkg/signal" 11 | "github.com/muxable/transcoder/api" 12 | "github.com/muxable/transcoder/internal/av" 13 | "github.com/muxable/transcoder/internal/codecs" 14 | "github.com/pion/interceptor" 15 | "github.com/pion/rtpio/pkg/rtpio" 16 | "github.com/pion/webrtc/v3" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | type Source struct { 21 | *webrtc.PeerConnection 22 | *webrtc.TrackRemote 23 | 24 | sinks []rtpio.RTPWriteCloser 25 | } 26 | 27 | func (s *Source) addSink(sink rtpio.RTPWriteCloser) { 28 | s.sinks = append(s.sinks, sink) 29 | if len(s.sinks) == 1 { 30 | go func() { 31 | dial, _ := net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127,0,0,1), Port: 5020}) 32 | for { 33 | p, _, err := s.TrackRemote.ReadRTP() 34 | if err != nil { 35 | for _, sink := range s.sinks { 36 | sink.Close() 37 | } 38 | return 39 | } 40 | buf, err := p.Marshal() 41 | if err != nil { 42 | zap.L().Error("failed to marshal rtp packet", zap.Error(err)) 43 | } 44 | dial.Write(buf) 45 | for _, sink := range s.sinks { 46 | if err := sink.WriteRTP(p); err != nil { 47 | zap.L().Error("failed to write rtp packet", zap.Error(err)) 48 | } 49 | } 50 | } 51 | }() 52 | } 53 | } 54 | 55 | type TranscoderServer struct { 56 | api.UnimplementedTranscoderServer 57 | config webrtc.Configuration 58 | 59 | // the transcoding server likely cannot process a huge number of remote tracks 60 | // so there's no need to optimize this. 61 | sources []*Source 62 | 63 | // this is like the poor man's rx behavior subject. 64 | onTrack *sync.Cond 65 | } 66 | 67 | func NewTranscoderServer(config webrtc.Configuration) *TranscoderServer { 68 | return &TranscoderServer{ 69 | config: config, 70 | onTrack: sync.NewCond(&sync.Mutex{}), 71 | } 72 | } 73 | 74 | func (s *TranscoderServer) Publish(conn api.Transcoder_PublishServer) error { 75 | m := &webrtc.MediaEngine{} 76 | 77 | // signal that we accept all the codecs. 78 | for _, codec := range codecs.DefaultOutputCodecs { 79 | if strings.HasPrefix(codec.MimeType, "video/") { 80 | if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil { 81 | return err 82 | } 83 | } else if strings.HasPrefix(codec.MimeType, "audio/") { 84 | if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil { 85 | return err 86 | } 87 | } 88 | } 89 | 90 | i := &interceptor.Registry{} 91 | if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { 92 | return err 93 | } 94 | 95 | peerConnection, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)).NewPeerConnection(s.config) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | signaller := signal.Negotiate(peerConnection) 101 | 102 | peerConnection.OnTrack(func(tr *webrtc.TrackRemote, r *webrtc.RTPReceiver) { 103 | go func() { 104 | buf := make([]byte, 1500) 105 | for { 106 | if _, _, err := r.Read(buf); err != nil { 107 | return 108 | } 109 | } 110 | }() 111 | 112 | source := &Source{ 113 | PeerConnection: peerConnection, 114 | TrackRemote: tr, 115 | } 116 | 117 | s.onTrack.L.Lock() 118 | s.sources = append(s.sources, source) 119 | s.onTrack.Broadcast() 120 | s.onTrack.L.Unlock() 121 | }) 122 | 123 | go func() { 124 | for { 125 | signal, err := signaller.ReadSignal() 126 | if err != nil { 127 | zap.L().Error("failed to read signal", zap.Error(err)) 128 | return 129 | } 130 | if err := conn.Send(signal); err != nil { 131 | zap.L().Error("failed to send signal", zap.Error(err)) 132 | return 133 | } 134 | } 135 | }() 136 | 137 | for { 138 | signal, err := conn.Recv() 139 | if err != nil { 140 | zap.L().Error("failed to receive", zap.Error(err)) 141 | return nil 142 | } 143 | 144 | if err := signaller.WriteSignal(signal); err != nil { 145 | zap.L().Error("failed to write signal", zap.Error(err), zap.String("signal", signal.String())) 146 | return nil 147 | } 148 | } 149 | } 150 | 151 | func (s *TranscoderServer) Subscribe(conn api.Transcoder_SubscribeServer) error { 152 | request, err := conn.Recv() 153 | if err != nil { 154 | return err 155 | } 156 | 157 | op, ok := request.Operation.(*api.SubscribeRequest_Request) 158 | if !ok { 159 | return errors.New("unexpected signal") 160 | } 161 | 162 | var matched *Source 163 | for matched == nil { 164 | s.onTrack.L.Lock() 165 | // find the track that matches the request. 166 | for i, source := range s.sources { 167 | tr := source.TrackRemote 168 | if tr.StreamID() == op.Request.StreamId && tr.ID() == op.Request.TrackId && tr.RID() == op.Request.RtpStreamId { 169 | matched = source 170 | s.sources = append(s.sources[:i], s.sources[i+1:]...) 171 | break 172 | } 173 | } 174 | 175 | if matched == nil { 176 | s.onTrack.Wait() 177 | } 178 | s.onTrack.L.Unlock() 179 | } 180 | 181 | inCodec := matched.TrackRemote.Codec() 182 | 183 | tc, err := av.NewTranscoder(inCodec, webrtc.RTPCodecCapability{}) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | matched.addSink(tc) 189 | 190 | outCodec := &webrtc.RTPCodecParameters{ 191 | PayloadType: webrtc.PayloadType(96), 192 | RTPCodecCapability: webrtc.RTPCodecCapability{ 193 | MimeType: webrtc.MimeTypeH264, 194 | ClockRate: 90000, 195 | }, 196 | } 197 | 198 | tl, err := webrtc.NewTrackLocalStaticRTP(outCodec.RTPCodecCapability, matched.TrackRemote.ID(), matched.TrackRemote.StreamID()) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | go rtpio.CopyRTP(tl, tc) 204 | 205 | m := &webrtc.MediaEngine{} 206 | 207 | if err := m.RegisterCodec(*outCodec, matched.TrackRemote.Kind()); err != nil { 208 | return err 209 | } 210 | 211 | i := &interceptor.Registry{} 212 | if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil { 213 | return err 214 | } 215 | 216 | peerConnection, err := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i)).NewPeerConnection(s.config) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | rtpSender, err := peerConnection.AddTrack(tl) 222 | if err != nil { 223 | return err 224 | } 225 | 226 | go func() { 227 | buf := make([]byte, 1500) 228 | for { 229 | if _, _, err := rtpSender.Read(buf); err != nil { 230 | return 231 | } 232 | } 233 | }() 234 | 235 | signaller := signal.Negotiate(peerConnection) 236 | 237 | go func() { 238 | for { 239 | signal, err := signaller.ReadSignal() 240 | if err != nil { 241 | zap.L().Error("failed to read signal", zap.Error(err)) 242 | return 243 | } 244 | 245 | log.Printf("received %v", signal) 246 | 247 | if err := conn.Send(signal); err != nil { 248 | zap.L().Error("failed to send signal", zap.Error(err)) 249 | return 250 | } 251 | } 252 | }() 253 | 254 | for { 255 | signal, err := conn.Recv() 256 | if err != nil { 257 | return err 258 | } 259 | 260 | log.Printf("received %v", signal) 261 | 262 | switch signal := signal.Operation.(type) { 263 | case *api.SubscribeRequest_Signal: 264 | if err := signaller.WriteSignal(signal.Signal); err != nil { 265 | return err 266 | } 267 | case *api.SubscribeRequest_Request: 268 | return errors.New("unexpected request") 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /test/e2e_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/muxable/transcoder/internal/codecs" 11 | "github.com/muxable/transcoder/pkg/transcoder" 12 | "github.com/pion/rtpio/pkg/rtpio" 13 | "github.com/pion/webrtc/v3" 14 | "go.uber.org/zap" 15 | "go.uber.org/zap/zaptest" 16 | ) 17 | 18 | func TestTranscoding(t *testing.T) { 19 | if _, ok := os.LookupEnv("DISPLAY"); !ok { 20 | t.Skip("Skipping testing in CI environment") 21 | } 22 | for mime, codec := range codecs.SupportedCodecs { 23 | t.Run(mime, func(t *testing.T) { 24 | if strings.HasPrefix(mime, "video") { 25 | runVideoTranscoder(t, mime, codec) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | func runVideoTranscoder(t *testing.T, mime string, codec codecs.GStreamerParameters) { 32 | logger := zaptest.NewLogger(t) 33 | defer logger.Sync() 34 | undo := zap.ReplaceGlobals(logger) 35 | defer undo() 36 | 37 | ptc, err := transcoder.NewTranscoder() 38 | if err != nil { 39 | t.Errorf("failed to create transcoder: %v", err) 40 | return 41 | } 42 | 43 | qtc, err := transcoder.NewTranscoder() 44 | if err != nil { 45 | t.Errorf("failed to create transcoder: %v", err) 46 | return 47 | } 48 | 49 | rtc, err := transcoder.NewTranscoder() 50 | if err != nil { 51 | t.Errorf("failed to create transcoder: %v", err) 52 | return 53 | } 54 | 55 | ic := codecs.DefaultOutputCodecs[webrtc.MimeTypeH264] 56 | p, err := ptc.NewReadOnlyPipeline(fmt.Sprintf("videotestsrc is-live=true num-buffers=100 ! video/x-raw,format=I420 ! x264enc ! rtph264pay pt=%d mtu=1200", ic.PayloadType)) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | pcodec, err := p.Codec() 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | qs, err := transcoder.NewPipelineBuilder(webrtc.RTPCodecTypeVideo, mime, "") 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | q, err := qtc.NewReadWritePipeline(pcodec, qs) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | go func() { 76 | if err := rtpio.CopyRTP(q, p); err != nil && err != io.EOF { 77 | t.Errorf("failed to copy rtp: %v", err) 78 | } 79 | q.Close() 80 | }() 81 | 82 | qcodec, err := q.Codec() 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | r, err := rtc.NewWriteOnlyPipeline(qcodec, "decodebin ! autovideosink") 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if err := rtpio.CopyRTP(r, q); err != nil && err != io.EOF { 93 | t.Errorf("failed to copy rtp: %v", err) 94 | } 95 | r.Close() 96 | } 97 | -------------------------------------------------------------------------------- /test/input.h265: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/input.h265 -------------------------------------------------------------------------------- /test/input.ivf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/input.ivf -------------------------------------------------------------------------------- /test/input.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/input.ogg -------------------------------------------------------------------------------- /test/output.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/output.avi -------------------------------------------------------------------------------- /test/output.h264: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/output.h264 -------------------------------------------------------------------------------- /test/output.h265: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/output.h265 -------------------------------------------------------------------------------- /test/output.ivf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/output.ivf -------------------------------------------------------------------------------- /test/output.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/output.ogg -------------------------------------------------------------------------------- /test/video.h265: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/transcoder/aad3afee1fbac12f0d503686c873fa56fc5e3414/test/video.h265 -------------------------------------------------------------------------------- /video264.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 0 0 IN IP4 127.0.0.1 3 | s=No Name 4 | c=IN IP4 127.0.0.1 5 | t=0 0 6 | a=tool:libavformat 58.29.100 7 | m=video 5001 RTP/AVP 96 8 | a=rtpmap:96 H264/90000 9 | a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f --------------------------------------------------------------------------------