├── .github └── ISSUE_TEMPLATE.md ├── 2020-06-27-webrtc.jpg ├── LICENSE ├── README.md ├── docs ├── code-of-conduct.md └── contributing.md ├── overview.jpg ├── recv-from-peer.go ├── recv-from-peer.svg ├── send-to-peer.go ├── send-to-peer.svg └── setup.go /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: -------------------------------------------------------------------------------- /2020-06-27-webrtc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stapelberg/costream/7946c4a6955fc864f2d6a605f3bbe558e37bb9a3/2020-06-27-webrtc.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains the early beginnings of a streaming setup that is 2 | suitable for co-streaming, pair-programming or whichever way you want to call 3 | two people collaborating on a piece of software on a live stream. 4 | 5 | Both people have their own twitch channels: https://www.twitch.tv/mdlayher and 6 | https://www.twitch.tv/stapelberg. So how do we combine them? 7 | 8 | Conceptually, we want to transport the OBS output (video and audio) to another 9 | computer, but with low latency. 10 | 11 | Unfortunately, the simplest solution of just using OBS to stream to a custom 12 | RTMP server turned out to have too much latency. 13 | 14 | ## First attempt: video call + twitch multistream 15 | 16 | In our first attempt ([recording](https://www.youtube.com/watch?v=JW8Cg6JDXSc)), 17 | we used https://whereby.com/ for a video call with screen sharing, and 18 | https://multistre.am/ to combine our two twitch channels, with audio only on one 19 | channel to have it synchronized. 20 | 21 | There were some issues with this approach: 22 | 23 | 1. Viewers of the stream without audio were confused and didn’t see the hint to 24 | watch at multistre.am. It would be better to have audio on both streams. 25 | 26 | 1. The OBS setup was pretty complicated: we needed to run multiple instances. 27 | 28 | ## Current attempt: gstreamer-based RTP 29 | 30 | thumbnail 32 | 33 | We knew from the first attempt that WebRTC as a technology works well for low 34 | latency streaming, but WebRTC screen sharing (as implemented in current video 35 | conference services) had some frustrating limitations: many services cap the 36 | refresh rate at 5 fps max, and most don’t allow for full 1920x1080 resolution. 37 | 38 | After several days of research and trial and error, we ended up using gstreamer 39 | to establish an RTP session with multiple streams, just like WebRTC does. 40 | 41 | This turned out to work really well ([recording](https://youtu.be/1g46ei9aBH0))! 42 | 43 | ## Overview 44 | 45 | ![overview](overview.jpg) 46 | 47 | ## Debugging 48 | 49 | To dump a dot graph out of gstreamer, see https://developer.ridgerun.com/wiki/index.php/How_to_generate_a_Gstreamer_pipeline_diagram_(graph) 50 | 51 | ## Limitation: one person cannot see the other, only hear 52 | 53 | There is plenty to improve about the experience, but in terms of conceptual 54 | limitations, the big one is that the person who is driving cannot see the other 55 | person (creating an infinite loop otherwise). 56 | 57 | ## OBS video setup 58 | 59 | Make OBS send its video output not just to stream and recording, but also to 60 | `/dev/video10`: 61 | 62 | * Tools 63 | * → v4l2sink 64 | * → device path: `/dev/video10` 65 | * → autostart enabled 66 | * → click start 67 | * → close window 68 | 69 | ## OBS audio setup 70 | 71 | First, point OBS’s monitoring feature to the monitor device of the `snd-loop` 72 | ALSA device, which we later grab via PulseAudio: 73 | 74 | * File 75 | * → Settings 76 | * → Audio 77 | * → Advanced 78 | * → Monitoring device: “Monitor Of Built-In Analog Stereo”. Unfortunately, our 79 | custom device name is not displayed by OBS. However, monitor devices are 80 | listed above their corresponding device, so hopefully that helps pick the 81 | right entry. 82 | 83 | Then, make OBS route your microphone into the monitoring device: 84 | 85 | * In the mixer at the bottom of the OBS window, right-click your mic 86 | * → Advanced Audio Properties 87 | * → Audio Monitoring 88 | * → Monitor and Output 89 | 90 | ## Dependencies 91 | 92 | ### Debian/Ubuntu 93 | 94 | | package | version number | 95 | |---------|----------------| 96 | | [v4l2loopback-dkms](https://packages.debian.org/bullseye/v4l2loopback-dkms) | 0.12.5-1 | 97 | | [obs-v4l2sink.deb](https://github.com/CatxFish/obs-v4l2sink/releases/download/0.1.0/obs-v4l2sink.deb) | 0.1.0 | 98 | | [gstreamer1.0-alsa](https://packages.ubuntu.com/bionic/gstreamer1.0-alsa) | 1.14.1 99 | 100 | ### Arch Linux 101 | 102 | | package | version number | 103 | |---------|----------------| 104 | | [community/v4l2loopback-dkms](https://www.archlinux.org/packages/community/any/v4l2loopback-dkms/) | 0.12.5-1 | 105 | | [AUR:obs-v4l2sink](https://aur.archlinux.org/packages/obs-v4l2sink/) | 0.1.0 | 106 | | [extra/gst-plugins-ugly](https://www.archlinux.org/packages/extra/x86_64/gst-plugins-ugly/) | 1.16.2-3 107 | 108 | ## Setup 109 | 110 | To set up the V4L2 devices `/dev/video10` and `/dev/video11`, as well as the 111 | ALSA `snd-aloop` loop device, run: 112 | 113 | ```shell 114 | go run setup.go 115 | ``` 116 | 117 | ## Sending/receiving a stream 118 | 119 | UDP port 5000 to 5007 need to be open. 120 | 121 | stapelberg runs: 122 | ``` 123 | # Read OBS stream video output and monitored audio output from: 124 | # -v4l2src_device=/dev/video10 and 125 | # -pulsesrc_device=alsa_output.platform-snd_aloop.0.analog-stereo.monitor 126 | go run send-to-peer.go -peer=rtp6.mdlayher.net.example -listen=rtp6.stapelberg.net.example 127 | 128 | # Write remote stream video/audio to: 129 | # -v4l2sink_device=/dev/video11 and 130 | # the default PulseAudio sink (desktop audio) 131 | go run recv-from-peer.go -peer=rtp6.mdlayher.net.example -listen=rtp6.stapelberg.net.example 132 | ``` 133 | 134 | Conversely, mdlayher runs: 135 | ``` 136 | go run send-to-peer.go -peer=rtp6.stapelberg.net.example -listen=rtp6.mdlayher.net.example 137 | go run recv-from-peer.go -peer=rtp6.stapelberg.net.example -listen=rtp6.mdlayher.net.example 138 | ``` 139 | -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Google Open Source Community Guidelines 2 | 3 | At Google, we recognize and celebrate the creativity and collaboration of open 4 | source contributors and the diversity of skills, experiences, cultures, and 5 | opinions they bring to the projects and communities they participate in. 6 | 7 | Every one of Google's open source projects and communities are inclusive 8 | environments, based on treating all individuals respectfully, regardless of 9 | gender identity and expression, sexual orientation, disabilities, 10 | neurodiversity, physical appearance, body size, ethnicity, nationality, race, 11 | age, religion, or similar personal characteristic. 12 | 13 | We value diverse opinions, but we value respectful behavior more. 14 | 15 | Respectful behavior includes: 16 | 17 | * Being considerate, kind, constructive, and helpful. 18 | * Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or 19 | physically threatening behavior, speech, and imagery. 20 | * Not engaging in unwanted physical contact. 21 | 22 | Some Google open source projects [may adopt][] an explicit project code of 23 | conduct, which may have additional detailed expectations for participants. Most 24 | of those projects will use our [modified Contributor Covenant][]. 25 | 26 | [may adopt]: https://opensource.google/docs/releasing/preparing/#conduct 27 | [modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ 28 | 29 | ## Resolve peacefully 30 | 31 | We do not believe that all conflict is necessarily bad; healthy debate and 32 | disagreement often yields positive results. However, it is never okay to be 33 | disrespectful. 34 | 35 | If you see someone behaving disrespectfully, you are encouraged to address the 36 | behavior directly with those involved. Many issues can be resolved quickly and 37 | easily, and this gives people more control over the outcome of their dispute. 38 | If you are unable to resolve the matter for any reason, or if the behavior is 39 | threatening or harassing, report it. We are dedicated to providing an 40 | environment where participants feel welcome and safe. 41 | 42 | ## Reporting problems 43 | 44 | Some Google open source projects may adopt a project-specific code of conduct. 45 | In those cases, a Google employee will be identified as the Project Steward, 46 | who will receive and handle reports of code of conduct violations. In the event 47 | that a project hasn’t identified a Project Steward, you can report problems by 48 | emailing opensource@google.com. 49 | 50 | We will investigate every complaint, but you may not receive a direct response. 51 | We will use our discretion in determining when and how to follow up on reported 52 | incidents, which may range from not taking action to permanent expulsion from 53 | the project and project-sponsored spaces. We will notify the accused of the 54 | report and provide them an opportunity to discuss it before any action is 55 | taken. The identity of the reporter will be omitted from the details of the 56 | report supplied to the accused. In potentially harmful situations, such as 57 | ongoing harassment or threats to anyone's safety, we may take action without 58 | notice. 59 | 60 | *This document was adapted from the [IndieWeb Code of Conduct][] and can also 61 | be found at .* 62 | 63 | [IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct 64 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stapelberg/costream/7946c4a6955fc864f2d6a605f3bbe558e37bb9a3/overview.jpg -------------------------------------------------------------------------------- /recv-from-peer.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2020 Google LLC 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // https://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "flag" 22 | "fmt" 23 | "log" 24 | "os" 25 | "os/exec" 26 | "os/signal" 27 | "syscall" 28 | ) 29 | 30 | // interruptibleContext returns a context which is canceled when the program is 31 | // interrupted (i.e. receiving SIGINT or SIGTERM). 32 | func interruptibleContext() (context.Context, context.CancelFunc) { 33 | ctx, canc := context.WithCancel(context.Background()) 34 | sig := make(chan os.Signal, 1) 35 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 36 | go func() { 37 | <-sig 38 | // Subsequent signals will result in immediate termination, which is 39 | // useful in case cleanup hangs: 40 | signal.Stop(sig) 41 | canc() 42 | }() 43 | return ctx, canc 44 | } 45 | 46 | func recvFromPeer(ctx context.Context) error { 47 | var ( 48 | peer = flag.String( 49 | "peer", 50 | "10.0.0.76", 51 | "TODO") 52 | 53 | listenAddr = flag.String( 54 | "listen", 55 | "midna.zekjur.net", 56 | "TODO") 57 | 58 | v4l2sinkdevice = flag.String( 59 | "v4l2sink_device", 60 | "/dev/video11", 61 | "device to send to peer") 62 | ) 63 | flag.Parse() 64 | 65 | // pipeline: source → filter → sink 66 | // - all of these are called gstreamer elements 67 | // - elements communicate with each other through pads 68 | // - source elements have a src pad 69 | // - filter elements have a src and a sink 70 | // - sink elements have a sink 71 | // - demuxer element has one sink pad (through which data arrives) and multiple source pads, one for each stream found in the container 72 | 73 | // TODO: is there a better way to describe gstreamer pipelines programmatically? 74 | // - could make the gstreamer pipeline description a bit nicer by using go types 75 | // - check pkg.go.dev if there already is an API for gstreamer in Go 76 | 77 | // TODO: ensure no other gst-launch processes are running based on a magic string in our args (e.g. rtpbin name) 78 | // - is this still necessary after switching to ctx? 79 | 80 | gst := exec.CommandContext(ctx, "gst-launch-1.0", 81 | "-v", 82 | // RTP session setup 83 | "rtpbin", "name=rtpbin", "latency=0", // default 200ms jitter buffer 84 | 85 | // UDP network setup 86 | "udpsrc", 87 | "address="+*listenAddr, 88 | "caps=application/x-rtp,media=(string)video,clock-rate=(int)90000,encoding-name=(string)H264", 89 | "port=5000", 90 | "!", "rtpbin.recv_rtp_sink_0", 91 | 92 | // Video setup 93 | "rtpbin.", 94 | "!", "rtph264depay", 95 | "!", "decodebin", 96 | "!", "videoconvert", 97 | "!", "v4l2sink", "device="+*v4l2sinkdevice, 98 | 99 | // More UDP network setup 100 | "udpsrc", "port=5001", 101 | "!", "rtpbin.recv_rtcp_sink_0", 102 | "rtpbin.send_rtcp_src_0", 103 | "!", "udpsink", "host="+*peer, "port=5005", "sync=false", "async=false", 104 | "udpsrc", 105 | "caps=application/x-rtp,media=(string)audio,clock-rate=(int)48000,encoding-name=(string)OPUS,encoding-params=(string)1,octet-align=(string)1", 106 | "address="+*listenAddr, 107 | "port=5002", 108 | "!", "rtpbin.recv_rtp_sink_1", 109 | 110 | // audio setup 111 | "rtpbin.", 112 | "!", "rtpopusdepay", 113 | "!", "opusdec", 114 | "!", "audioconvert", 115 | "!", "audioresample", 116 | // Output to default PulseAudio sink, which will be captured by OBS as 117 | // desktop audio. This is a little better than introducing another loop 118 | // device hop. 119 | "!", "pulsesink", 120 | 121 | // Even more UDP network setup 122 | "udpsrc", "port=5003", "!", "rtpbin.recv_rtcp_sink_1", 123 | "rtpbin.send_rtcp_src_1", "!", "udpsink", "host="+*peer, "port=5007", "sync=false", "async=false") 124 | 125 | gst.Stdout = os.Stdout 126 | gst.Stderr = os.Stderr 127 | log.Println(gst.Args) 128 | // TODO(later): read rtpsession stats line by line and print an interactive progress like mpv 129 | if err := gst.Run(); err != nil { 130 | return fmt.Errorf("%v: %v", gst.Args, err) 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func main() { 137 | ctx, canc := interruptibleContext() 138 | defer canc() 139 | if err := recvFromPeer(ctx); err != nil { 140 | log.Fatal(err) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /send-to-peer.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2020 Google LLC 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // https://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "flag" 22 | "fmt" 23 | "log" 24 | "os" 25 | "os/exec" 26 | ) 27 | 28 | func sendToPeer(ctx context.Context) error { 29 | var ( 30 | peer = flag.String( 31 | "peer", 32 | "10.0.0.66", 33 | "TODO") 34 | 35 | listenAddr = flag.String( 36 | "listen", 37 | "midna.zekjur.net", 38 | "TODO") 39 | 40 | v4l2srcdevice = flag.String( 41 | "v4l2src_device", 42 | "/dev/video10", 43 | "V4L2 video device to send to peer") 44 | 45 | pulsesrcdevice = flag.String( 46 | "pulsesrc_device", 47 | // cannot open ALSA subdevice ,1 via pulse, so we just use pulseaudio’s monitor 48 | // mic: alsa_input.usb-RODE_MICROPHONESj_Rode_Podcaster-00.mono-fallback 49 | "alsa_output.platform-snd_aloop.0.analog-stereo.monitor", 50 | "PulseAudio source (see pactl list sources) to send to peer") 51 | ) 52 | flag.Parse() 53 | 54 | // pipeline: source → filter → sink 55 | // - all of these are called gstreamer elements 56 | // - elements communicate with each other through pads 57 | // - source elements have a src pad 58 | // - filter elements have a src and a sink 59 | // - sink elements have a sink 60 | // - demuxer element has one sink pad (through which data arrives) and multiple source pads, one for each stream found in the container 61 | 62 | // TODO: is there a better way to describe gstreamer pipelines programmatically? 63 | // - could make the gstreamer pipeline description a bit nicer by using go types 64 | // - check pkg.go.dev if there already is an API for gstreamer in Go 65 | 66 | gst := exec.Command("gst-launch-1.0", 67 | "-v", 68 | // RTP session setup 69 | "rtpbin", "name=rtpbin", "latency=0", // default 200ms jit 70 | // TODO: max-rtcp-rtp-time-diff=50? 71 | 72 | // Video setup 73 | "v4l2src", "device="+*v4l2srcdevice, 74 | "!", "video/x-raw,width=1920,height=1080,framerate=30/1", 75 | "!", "videoscale", 76 | "!", "videoconvert", 77 | // TODO: queue here? or elsewhere in the video stage? 78 | "!", "x264enc", 79 | "tune=zerolatency", 80 | "bitrate=1000", 81 | "speed-preset=ultrafast", 82 | // TODO: intra-refresh=true? 83 | // TODO: quantizer=30? 84 | // TODO: pass=5? 85 | "!", "rtph264pay", 86 | "!", "rtpbin.send_rtp_sink_0", 87 | 88 | // Send video RTP to :5000 89 | "rtpbin.send_rtp_src_0", 90 | "!", "udpsink", "host="+*peer, "port=5000", 91 | // https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-good/html/gst-plugins-good-plugins-rtpbin.html#id-1.2.136.9.12 92 | // Since RTCP packets from the sender should be sent as soon as possible 93 | // and do not participate in preroll, sync=false and async=false is 94 | // configured on udpsink 95 | "rtpbin.send_rtcp_src_0", 96 | "!", "udpsink", "host="+*peer, "port=5001", "sync=false", "async=false", 97 | // Receive RTCP packets for video stream on :5005 98 | "udpsrc", "address="+*listenAddr, "port=5005", 99 | "!", "rtpbin.recv_rtcp_sink_0", 100 | 101 | // Audio setup 102 | // TODO: for lower latency, could capture the microphone directly, 103 | // but have to set up microphone filters on the remote OBS 104 | "pulsesrc", "device="+*pulsesrcdevice, 105 | "!", "audio/x-raw,rate=44100", 106 | "!", "audioresample", 107 | "!", "opusenc", "audio-type=voice", 108 | "!", "rtpopuspay", 109 | "!", "rtpbin.send_rtp_sink_1", 110 | 111 | // Send audio RTP to :5002 112 | "rtpbin.send_rtp_src_1", "!", "udpsink", "host="+*peer, "port=5002", 113 | "rtpbin.send_rtcp_src_1", "!", "udpsink", "host="+*peer, "port=5003", "sync=false", "async=false", 114 | "udpsrc", "address="+*listenAddr, "port=5007", "!", "rtpbin.recv_rtcp_sink_1") 115 | gst.Stdout = os.Stdout 116 | gst.Stderr = os.Stderr 117 | log.Println(gst.Args) 118 | // TODO(later): read rtpsession stats line by line and print an interactive progress like mpv 119 | if err := gst.Run(); err != nil { 120 | return fmt.Errorf("%v: %v", gst.Args, err) 121 | } 122 | return nil 123 | } 124 | 125 | func main() { 126 | if err := sendToPeer(context.Background()); err != nil { 127 | log.Fatal(err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /send-to-peer.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | pipeline 11 | 12 | <GstPipeline> 13 | pipeline0 14 | [=] 15 | 16 | cluster_capsfilter1_0x555ad52908a0 17 | 18 | GstCapsFilter 19 | capsfilter1 20 | [=] 21 | parent=(GstPipeline) pipeline0 22 | caps=audio/x-raw, rate=(int)44100 23 | 24 | 25 | cluster_capsfilter1_0x555ad52908a0_sink 26 | 27 | 28 | cluster_capsfilter1_0x555ad52908a0_src 29 | 30 | 31 | cluster_capsfilter0_0x555ad5290220 32 | 33 | GstCapsFilter 34 | capsfilter0 35 | [=] 36 | parent=(GstPipeline) pipeline0 37 | caps=video/x-raw, width=(int)1920, height=(int)1080, framerate=(fraction)30/1 38 | 39 | 40 | cluster_capsfilter0_0x555ad5290220_sink 41 | 42 | 43 | cluster_capsfilter0_0x555ad5290220_src 44 | 45 | 46 | cluster_udpsrc1_0x555ad528a720 47 | 48 | GstUDPSrc 49 | udpsrc1 50 | [=] 51 | parent=(GstPipeline) pipeline0 52 | do-timestamp=TRUE 53 | port=5007 54 | uri="udp://0.0.0.0:5007" 55 | used-socket=((GSocket*) 0x555ad52b5d00) 56 | 57 | 58 | cluster_udpsrc1_0x555ad528a720_src 59 | 60 | 61 | cluster_udpsink3_0x555ad52899f0 62 | 63 | GstUDPSink 64 | udpsink3 65 | [=] 66 | parent=(GstPipeline) pipeline0 67 | sync=FALSE 68 | async=FALSE 69 | last-sample=((GstSample*) 0x555ad51f5e40) 70 | bytes-to-serve=320 71 | bytes-served=320 72 | used-socket=((GSocket*) 0x555ad52b5130) 73 | used-socket-v6=((GSocket*) 0x555ad52b5280) 74 | clients="10.0.0.66:5003" 75 | host="10.0.0.66" 76 | port=5003 77 | 78 | 79 | cluster_udpsink3_0x555ad52899f0_sink 80 | 81 | 82 | cluster_udpsink2_0x555ad52881f0 83 | 84 | GstUDPSink 85 | udpsink2 86 | [=] 87 | parent=(GstPipeline) pipeline0 88 | last-sample=((GstSample*) 0x555ad5211860) 89 | bytes-to-serve=180256 90 | bytes-served=180256 91 | used-socket=((GSocket*) 0x555ad52b53d0) 92 | used-socket-v6=((GSocket*) 0x555ad52b5520) 93 | clients="10.0.0.66:5002" 94 | host="10.0.0.66" 95 | port=5002 96 | 97 | 98 | cluster_udpsink2_0x555ad52881f0_sink 99 | 100 | 101 | cluster_rtpopuspay0_0x555ad5286080 102 | 103 | GstRtpOPUSPay 104 | rtpopuspay0 105 | [=] 106 | parent=(GstPipeline) pipeline0 107 | timestamp=1369112811 108 | seqnum=28070 109 | stats=application/x-rtp-payload-stats, clock-rate=(uint)48000, running-time=(guint64)1… 110 | 111 | 112 | cluster_rtpopuspay0_0x555ad5286080_sink 113 | 114 | 115 | cluster_rtpopuspay0_0x555ad5286080_src 116 | 117 | 118 | cluster_opusenc0_0x555ad5283a90 119 | 120 | GstOpusEnc 121 | opusenc0 122 | [=] 123 | parent=(GstPipeline) pipeline0 124 | audio-type=voice 125 | 126 | 127 | cluster_opusenc0_0x555ad5283a90_sink 128 | 129 | 130 | cluster_opusenc0_0x555ad5283a90_src 131 | 132 | 133 | cluster_audioresample0_0x555ad527de10 134 | 135 | GstAudioResample 136 | audioresample0 137 | [=] 138 | parent=(GstPipeline) pipeline0 139 | 140 | 141 | cluster_audioresample0_0x555ad527de10_sink 142 | 143 | 144 | cluster_audioresample0_0x555ad527de10_src 145 | 146 | 147 | cluster_pulsesrc0_0x555ad5277230 148 | 149 | GstPulseSrc 150 | pulsesrc0 151 | [=] 152 | parent=(GstPipeline) pipeline0 153 | blocksize=0 154 | actual-buffer-time=200000 155 | actual-latency-time=10000 156 | device="alsa_input.usb-RODE_MICROPHONESj_Rode_Podcaster-00.mono-fallback" 157 | device-name="Podcaster Mono" 158 | current-device="alsa_input.usb-RODE_MICROPHONESj_Rode_Podcaster-00.mono-fallback" 159 | source-output-index=22 160 | 161 | 162 | cluster_pulsesrc0_0x555ad5277230_src 163 | 164 | 165 | cluster_udpsrc0_0x555ad526c1d0 166 | 167 | GstUDPSrc 168 | udpsrc0 169 | [=] 170 | parent=(GstPipeline) pipeline0 171 | do-timestamp=TRUE 172 | port=5005 173 | uri="udp://0.0.0.0:5005" 174 | used-socket=((GSocket*) 0x555ad52b5bb0) 175 | 176 | 177 | cluster_udpsrc0_0x555ad526c1d0_src 178 | 179 | 180 | cluster_udpsink1_0x555ad526a560 181 | 182 | GstUDPSink 183 | udpsink1 184 | [=] 185 | parent=(GstPipeline) pipeline0 186 | sync=FALSE 187 | async=FALSE 188 | last-sample=((GstSample*) 0x555ad5211940) 189 | bytes-to-serve=320 190 | bytes-served=320 191 | used-socket=((GSocket*) 0x555ad52b5670) 192 | used-socket-v6=((GSocket*) 0x555ad52b57c0) 193 | clients="10.0.0.66:5001" 194 | host="10.0.0.66" 195 | port=5001 196 | 197 | 198 | cluster_udpsink1_0x555ad526a560_sink 199 | 200 | 201 | cluster_udpsink0_0x555ad52675f0 202 | 203 | GstUDPSink 204 | udpsink0 205 | [=] 206 | parent=(GstPipeline) pipeline0 207 | last-sample=((GstSample*) 0x555ad5211a20) 208 | bytes-to-serve=1567165 209 | bytes-served=1567165 210 | used-socket=((GSocket*) 0x555ad52b5910) 211 | used-socket-v6=((GSocket*) 0x555ad52b5a60) 212 | clients="10.0.0.66:5000" 213 | host="10.0.0.66" 214 | port=5000 215 | 216 | 217 | cluster_udpsink0_0x555ad52675f0_sink 218 | 219 | 220 | cluster_rtph264pay0_0x555ad525e130 221 | 222 | GstRtpH264Pay 223 | rtph264pay0 224 | [=] 225 | parent=(GstPipeline) pipeline0 226 | timestamp=2284676457 227 | seqnum=7987 228 | stats=application/x-rtp-payload-stats, clock-rate=(uint)90000, running-time=(guint64)1… 229 | 230 | 231 | cluster_rtph264pay0_0x555ad525e130_sink 232 | 233 | 234 | cluster_rtph264pay0_0x555ad525e130_src 235 | 236 | 237 | cluster_x264enc0_0x555ad5254120 238 | 239 | GstX264Enc 240 | x264enc0 241 | [=] 242 | parent=(GstPipeline) pipeline0 243 | bitrate=1000 244 | speed-preset=ultrafast 245 | tune=zerolatency 246 | 247 | 248 | cluster_x264enc0_0x555ad5254120_sink 249 | 250 | 251 | cluster_x264enc0_0x555ad5254120_src 252 | 253 | 254 | cluster_videoconvert0_0x555ad524d050 255 | 256 | GstVideoConvert 257 | videoconvert0 258 | [=] 259 | parent=(GstPipeline) pipeline0 260 | qos=TRUE 261 | 262 | 263 | cluster_videoconvert0_0x555ad524d050_sink 264 | 265 | 266 | cluster_videoconvert0_0x555ad524d050_src 267 | 268 | 269 | cluster_videoscale0_0x555ad523aa50 270 | 271 | GstVideoScale 272 | videoscale0 273 | [=] 274 | parent=(GstPipeline) pipeline0 275 | qos=TRUE 276 | 277 | 278 | cluster_videoscale0_0x555ad523aa50_sink 279 | 280 | 281 | cluster_videoscale0_0x555ad523aa50_src 282 | 283 | 284 | cluster_v4l2src0_0x555ad523c100 285 | 286 | GstV4l2Src 287 | v4l2src0 288 | [=] 289 | parent=(GstPipeline) pipeline0 290 | device="/dev/video10" 291 | device-name="v4l2-topeer" 292 | device-fd=31 293 | flags=capture 294 | pixel-aspect-ratio=NULL 295 | 296 | 297 | cluster_v4l2src0_0x555ad523c100_src 298 | 299 | 300 | cluster_rtpbin_0x555ad5206050 301 | 302 | GstRtpBin 303 | rtpbin 304 | [=] 305 | parent=(GstPipeline) pipeline0 306 | latency=0 307 | sdes=application/x-rtp-source-sdes, cname=(string)\"user3969674431\\@host-d336821… 308 | 309 | 310 | cluster_rtpbin_0x555ad5206050_sink 311 | 312 | 313 | cluster_rtpbin_0x555ad5206050_src 314 | 315 | 316 | cluster_rtpstorage1_0x555ad5261360 317 | 318 | GstRtpStorage 319 | rtpstorage1 320 | [=] 321 | parent=(GstRtpBin) rtpbin 322 | internal-storage=((RtpStorage*) 0x555ad52ac530) 323 | 324 | 325 | cluster_rtpstorage1_0x555ad5261360_sink 326 | 327 | 328 | cluster_rtpstorage1_0x555ad5261360_src 329 | 330 | 331 | cluster_rtpssrcdemux1_0x555ad5133c00 332 | 333 | GstRtpSsrcDemux 334 | rtpssrcdemux1 335 | [=] 336 | parent=(GstRtpBin) rtpbin 337 | 338 | 339 | cluster_rtpssrcdemux1_0x555ad5133c00_sink 340 | 341 | 342 | cluster_rtpsession1_0x555ad529a510 343 | 344 | GstRtpSession 345 | rtpsession1 346 | [=] 347 | parent=(GstRtpBin) rtpbin 348 | sdes=application/x-rtp-source-sdes, cname=(string)\"user3969674431\\@host-d336821… 349 | num-sources=2 350 | num-active-sources=2 351 | internal-session=((RTPSession*) 0x555ad5290b10) 352 | stats=application/x-rtp-session-stats, rtx-drop-count=(uint)0, sent-nack-count=(uint)0… 353 | 354 | 355 | cluster_rtpsession1_0x555ad529a510_sink 356 | 357 | 358 | cluster_rtpsession1_0x555ad529a510_src 359 | 360 | 361 | cluster_rtpstorage0_0x555ad5261240 362 | 363 | GstRtpStorage 364 | rtpstorage0 365 | [=] 366 | parent=(GstRtpBin) rtpbin 367 | internal-storage=((RtpStorage*) 0x555ad5294070) 368 | 369 | 370 | cluster_rtpstorage0_0x555ad5261240_sink 371 | 372 | 373 | cluster_rtpstorage0_0x555ad5261240_src 374 | 375 | 376 | cluster_rtpssrcdemux0_0x555ad5133ad0 377 | 378 | GstRtpSsrcDemux 379 | rtpssrcdemux0 380 | [=] 381 | parent=(GstRtpBin) rtpbin 382 | 383 | 384 | cluster_rtpssrcdemux0_0x555ad5133ad0_sink 385 | 386 | 387 | cluster_rtpsession0_0x555ad529a260 388 | 389 | GstRtpSession 390 | rtpsession0 391 | [=] 392 | parent=(GstRtpBin) rtpbin 393 | sdes=application/x-rtp-source-sdes, cname=(string)\"user3969674431\\@host-d336821… 394 | num-sources=2 395 | num-active-sources=2 396 | internal-session=((RTPSession*) 0x555ad5290490) 397 | stats=application/x-rtp-session-stats, rtx-drop-count=(uint)0, sent-nack-count=(uint)0… 398 | 399 | 400 | cluster_rtpsession0_0x555ad529a260_sink 401 | 402 | 403 | cluster_rtpsession0_0x555ad529a260_src 404 | 405 | 406 | 407 | legend 408 | 409 | Legend 410 | Element-States: [~] void-pending, [0] null, [-] ready, [=] paused, [>] playing 411 | Pad-Activation: [-] none, [>] push, [<] pull 412 | Pad-Flags: [b]locked, [f]lushing, [b]locking, [E]OS; upper-case is set 413 | Pad-Task: [T] has started task, [t] has paused task 414 | 415 | 416 | 417 | capsfilter1_0x555ad52908a0_sink_0x555ad529f190 418 | 419 | sink 420 | [>][bfb] 421 | 422 | 423 | 424 | capsfilter1_0x555ad52908a0_src_0x555ad529f3e0 425 | 426 | src 427 | [>][bfb] 428 | 429 | 430 | 431 | 432 | audioresample0_0x555ad527de10_sink_0x555ad5278100 433 | 434 | sink 435 | [>][bfb] 436 | 437 | 438 | 439 | capsfilter1_0x555ad52908a0_src_0x555ad529f3e0->audioresample0_0x555ad527de10_sink_0x555ad5278100 440 | 441 | 442 | audio/x-raw 443 |              format: S16LE 444 |              layout: interleaved 445 |                rate: 44100 446 |            channels: 1 447 | 448 | 449 | 450 | audioresample0_0x555ad527de10_src_0x555ad5278350 451 | 452 | src 453 | [>][bfb] 454 | 455 | 456 | 457 | 458 | capsfilter0_0x555ad5290220_sink_0x555ad52795d0 459 | 460 | sink 461 | [>][bfb] 462 | 463 | 464 | 465 | capsfilter0_0x555ad5290220_src_0x555ad5279820 466 | 467 | src 468 | [>][bfb] 469 | 470 | 471 | 472 | 473 | videoscale0_0x555ad523aa50_sink_0x555ad5246380 474 | 475 | sink 476 | [>][bfb] 477 | 478 | 479 | 480 | capsfilter0_0x555ad5290220_src_0x555ad5279820->videoscale0_0x555ad523aa50_sink_0x555ad5246380 481 | 482 | 483 | video/x-raw 484 |               width: 1920 485 |              height: 1080 486 |           framerate: 30/1 487 |              format: I420 488 |         colorimetry: 2:4:7:1 489 |      interlace-mode: progressive 490 | 491 | 492 | 493 | videoscale0_0x555ad523aa50_src_0x555ad52465d0 494 | 495 | src 496 | [>][bfb] 497 | 498 | 499 | 500 | 501 | udpsrc1_0x555ad528a720_src_0x555ad5279380 502 | 503 | src 504 | [>][bfb][T] 505 | 506 | 507 | 508 | rtpbin_0x555ad5206050_recv_rtcp_sink_1_0x555ad52a52a0 509 | 510 | recv_rtcp_sink_1 511 | [>][bfb] 512 | 513 | 514 | 515 | udpsrc1_0x555ad528a720_src_0x555ad5279380->rtpbin_0x555ad5206050_recv_rtcp_sink_1_0x555ad52a52a0 516 | 517 | 518 |                                                   519 | application/x-rtcp 520 | application/x-srtcp 521 | ANY 522 | 523 | 524 | 525 | _proxypad7_0x555ad52a7100 526 | 527 | proxypad7 528 | [>][bfb] 529 | 530 | 531 | 532 | rtpbin_0x555ad5206050_recv_rtcp_sink_1_0x555ad52a52a0->_proxypad7_0x555ad52a7100 533 | 534 | 535 | 536 | 537 | 538 | udpsink3_0x555ad52899f0_sink_0x555ad5279130 539 | 540 | sink 541 | [>][bfb] 542 | 543 | 544 | 545 | udpsink2_0x555ad52881f0_sink_0x555ad5278ee0 546 | 547 | sink 548 | [>][bfb] 549 | 550 | 551 | 552 | rtpopuspay0_0x555ad5286080_sink_0x555ad5278c90 553 | 554 | sink 555 | [>][bfb] 556 | 557 | 558 | 559 | rtpopuspay0_0x555ad5286080_src_0x555ad5278a40 560 | 561 | src 562 | [>][bfb] 563 | 564 | 565 | 566 | 567 | rtpbin_0x555ad5206050_send_rtp_sink_1_0x555ad52a4da0 568 | 569 | send_rtp_sink_1 570 | [>][bfb] 571 | 572 | 573 | 574 | rtpopuspay0_0x555ad5286080_src_0x555ad5278a40->rtpbin_0x555ad5206050_send_rtp_sink_1_0x555ad52a4da0 575 | 576 | 577 | application/x-rtp 578 |               media: audio 579 |          clock-rate: 48000 580 |       encoding-name: OPUS 581 |  sprop-maxcapturerate: 48000 582 |        sprop-stereo: 0 583 |             payload: 96 584 |     encoding-params: 2 585 |                ssrc: 4177561214 586 |    timestamp-offset: 1368107043 587 |       seqnum-offset: 27022 588 | 589 | 590 | 591 | _proxypad5_0x555ad52a6c40 592 | 593 | proxypad5 594 | [>][bfb] 595 | 596 | 597 | 598 | rtpbin_0x555ad5206050_send_rtp_sink_1_0x555ad52a4da0->_proxypad5_0x555ad52a6c40 599 | 600 | 601 | 602 | 603 | 604 | opusenc0_0x555ad5283a90_sink_0x555ad52785a0 605 | 606 | sink 607 | [>][bfb] 608 | 609 | 610 | 611 | opusenc0_0x555ad5283a90_src_0x555ad52787f0 612 | 613 | src 614 | [>][bfb] 615 | 616 | 617 | 618 | 619 | opusenc0_0x555ad5283a90_src_0x555ad52787f0->rtpopuspay0_0x555ad5286080_sink_0x555ad5278c90 620 | 621 | 622 | audio/x-opus 623 |                rate: 48000 624 |            channels: 1 625 |  channel-mapping-family: 0 626 |        stream-count: 1 627 |       coupled-count: 0 628 |        streamheader: < (buffer)4f70757348... > 629 | 630 | 631 | 632 | audioresample0_0x555ad527de10_src_0x555ad5278350->opusenc0_0x555ad5283a90_sink_0x555ad52785a0 633 | 634 | 635 | audio/x-raw 636 |                rate: 48000 637 |            channels: 1 638 |              format: S16LE 639 |              layout: interleaved 640 | 641 | 642 | 643 | pulsesrc0_0x555ad5277230_src_0x555ad5247cf0 644 | 645 | src 646 | [>][bfb][T] 647 | 648 | 649 | 650 | pulsesrc0_0x555ad5277230_src_0x555ad5247cf0->capsfilter1_0x555ad52908a0_sink_0x555ad529f190 651 | 652 | 653 | audio/x-raw 654 |              format: S16LE 655 |              layout: interleaved 656 |                rate: 44100 657 |            channels: 1 658 | 659 | 660 | 661 | udpsrc0_0x555ad526c1d0_src_0x555ad5247aa0 662 | 663 | src 664 | [>][bfb][T] 665 | 666 | 667 | 668 | rtpbin_0x555ad5206050_recv_rtcp_sink_0_0x555ad52a48a0 669 | 670 | recv_rtcp_sink_0 671 | [>][bfb] 672 | 673 | 674 | 675 | udpsrc0_0x555ad526c1d0_src_0x555ad5247aa0->rtpbin_0x555ad5206050_recv_rtcp_sink_0_0x555ad52a48a0 676 | 677 | 678 |                                                   679 | application/x-rtcp 680 | application/x-srtcp 681 | ANY 682 | 683 | 684 | 685 | _proxypad3_0x555ad52a6780 686 | 687 | proxypad3 688 | [>][bfb] 689 | 690 | 691 | 692 | rtpbin_0x555ad5206050_recv_rtcp_sink_0_0x555ad52a48a0->_proxypad3_0x555ad52a6780 693 | 694 | 695 | 696 | 697 | 698 | udpsink1_0x555ad526a560_sink_0x555ad5247850 699 | 700 | sink 701 | [>][bfb] 702 | 703 | 704 | 705 | udpsink0_0x555ad52675f0_sink_0x555ad5247600 706 | 707 | sink 708 | [>][bfb] 709 | 710 | 711 | 712 | rtph264pay0_0x555ad525e130_sink_0x555ad52473b0 713 | 714 | sink 715 | [>][bfb] 716 | 717 | 718 | 719 | rtph264pay0_0x555ad525e130_src_0x555ad5247160 720 | 721 | src 722 | [>][bfb] 723 | 724 | 725 | 726 | 727 | rtpbin_0x555ad5206050_send_rtp_sink_0_0x555ad52a43a0 728 | 729 | send_rtp_sink_0 730 | [>][bfb] 731 | 732 | 733 | 734 | rtph264pay0_0x555ad525e130_src_0x555ad5247160->rtpbin_0x555ad5206050_send_rtp_sink_0_0x555ad52a43a0 735 | 736 | 737 | application/x-rtp 738 |               media: video 739 |          clock-rate: 90000 740 |       encoding-name: H264 741 |  packetization-mode: 1 742 |    profile-level-id: 42c028 743 |  sprop-parameter-sets: "Z0LAKNoB4AiflwFqAgQ... " 744 |             payload: 96 745 |                ssrc: 1156415737 746 |    timestamp-offset: 2282790541 747 |       seqnum-offset: 687 748 |         a-framerate: 30 749 | 750 | 751 | 752 | _proxypad1_0x555ad52a62c0 753 | 754 | proxypad1 755 | [>][bfb] 756 | 757 | 758 | 759 | rtpbin_0x555ad5206050_send_rtp_sink_0_0x555ad52a43a0->_proxypad1_0x555ad52a62c0 760 | 761 | 762 | 763 | 764 | 765 | rtpbin_0x555ad5206050_send_rtp_src_0_0x555ad52a4120 766 | 767 | send_rtp_src_0 768 | [>][bfb] 769 | 770 | 771 | 772 | 773 | x264enc0_0x555ad5254120_sink_0x555ad5246cc0 774 | 775 | sink 776 | [>][bfb] 777 | 778 | 779 | 780 | x264enc0_0x555ad5254120_src_0x555ad5246f10 781 | 782 | src 783 | [>][bfb] 784 | 785 | 786 | 787 | 788 | x264enc0_0x555ad5254120_src_0x555ad5246f10->rtph264pay0_0x555ad525e130_sink_0x555ad52473b0 789 | 790 | 791 | video/x-h264 792 |          codec_data: 0142c028ffe1001c6742c0... 793 |       stream-format: avc 794 |           alignment: au 795 |               level: 4 796 |             profile: constrained-baseline 797 |               width: 1920 798 |              height: 1080 799 |  pixel-aspect-ratio: 1/1 800 |           framerate: 30/1 801 |      interlace-mode: progressive 802 |         colorimetry: 2:4:7:1 803 | 804 | 805 | 806 | videoconvert0_0x555ad524d050_sink_0x555ad5246820 807 | 808 | sink 809 | [>][bfb] 810 | 811 | 812 | 813 | videoconvert0_0x555ad524d050_src_0x555ad5246a70 814 | 815 | src 816 | [>][bfb] 817 | 818 | 819 | 820 | 821 | videoconvert0_0x555ad524d050_src_0x555ad5246a70->x264enc0_0x555ad5254120_sink_0x555ad5246cc0 822 | 823 | 824 | video/x-raw 825 |               width: 1920 826 |              height: 1080 827 |           framerate: 30/1 828 |              format: I420 829 |         colorimetry: 2:4:7:1 830 |      interlace-mode: progressive 831 | 832 | 833 | 834 | videoscale0_0x555ad523aa50_src_0x555ad52465d0->videoconvert0_0x555ad524d050_sink_0x555ad5246820 835 | 836 | 837 | video/x-raw 838 |               width: 1920 839 |              height: 1080 840 |           framerate: 30/1 841 |              format: I420 842 |         colorimetry: 2:4:7:1 843 |      interlace-mode: progressive 844 | 845 | 846 | 847 | v4l2src0_0x555ad523c100_src_0x555ad5246130 848 | 849 | src 850 | [>][bfb][T] 851 | 852 | 853 | 854 | v4l2src0_0x555ad523c100_src_0x555ad5246130->capsfilter0_0x555ad5290220_sink_0x555ad52795d0 855 | 856 | 857 | video/x-raw 858 |               width: 1920 859 |              height: 1080 860 |           framerate: 30/1 861 |              format: I420 862 |         colorimetry: 2:4:7:1 863 |      interlace-mode: progressive 864 | 865 | 866 | 867 | rtpsession0_0x555ad529a260_send_rtp_sink_0x555ad529e600 868 | 869 | send_rtp_sink 870 | [>][bfb] 871 | 872 | 873 | 874 | _proxypad1_0x555ad52a62c0->rtpsession0_0x555ad529a260_send_rtp_sink_0x555ad529e600 875 | 876 | 877 | application/x-rtp 878 |               media: video 879 |          clock-rate: 90000 880 |       encoding-name: H264 881 |  packetization-mode: 1 882 |    profile-level-id: 42c028 883 |  sprop-parameter-sets: "Z0LAKNoB4AiflwFqAgQ... " 884 |             payload: 96 885 |                ssrc: 1156415737 886 |    timestamp-offset: 2282790541 887 |       seqnum-offset: 687 888 |         a-framerate: 30 889 | 890 | 891 | 892 | rtpsession0_0x555ad529a260_recv_rtcp_sink_0x555ad529ecf0 893 | 894 | recv_rtcp_sink 895 | [>][bfb] 896 | 897 | 898 | 899 | _proxypad3_0x555ad52a6780->rtpsession0_0x555ad529a260_recv_rtcp_sink_0x555ad529ecf0 900 | 901 | 902 |                                                   903 | application/x-rtcp 904 | application/x-rtcp 905 | application/x-srtcp 906 | 907 | 908 | 909 | rtpsession1_0x555ad529a510_send_rtp_sink_0x555ad52b01e0 910 | 911 | send_rtp_sink 912 | [>][bfb] 913 | 914 | 915 | 916 | _proxypad5_0x555ad52a6c40->rtpsession1_0x555ad529a510_send_rtp_sink_0x555ad52b01e0 917 | 918 | 919 | application/x-rtp 920 |               media: audio 921 |          clock-rate: 48000 922 |       encoding-name: OPUS 923 |  sprop-maxcapturerate: 48000 924 |        sprop-stereo: 0 925 |             payload: 96 926 |     encoding-params: 2 927 |                ssrc: 4177561214 928 |    timestamp-offset: 1368107043 929 |       seqnum-offset: 27022 930 | 931 | 932 | 933 | rtpsession1_0x555ad529a510_recv_rtcp_sink_0x555ad52b08d0 934 | 935 | recv_rtcp_sink 936 | [>][bfb] 937 | 938 | 939 | 940 | _proxypad7_0x555ad52a7100->rtpsession1_0x555ad529a510_recv_rtcp_sink_0x555ad52b08d0 941 | 942 | 943 |                                                   944 | application/x-rtcp 945 | application/x-rtcp 946 | application/x-srtcp 947 | 948 | 949 | 950 | _proxypad0_0x555ad52a6060 951 | 952 | proxypad0 953 | [>][bfb] 954 | 955 | 956 | 957 | _proxypad0_0x555ad52a6060->rtpbin_0x555ad5206050_send_rtp_src_0_0x555ad52a4120 958 | 959 | 960 | 961 | 962 | 963 | rtpbin_0x555ad5206050_send_rtp_src_0_0x555ad52a4120->udpsink0_0x555ad52675f0_sink_0x555ad5247600 964 | 965 | 966 | application/x-rtp 967 |               media: video 968 |          clock-rate: 90000 969 |       encoding-name: H264 970 |  packetization-mode: 1 971 |    profile-level-id: 42c028 972 |  sprop-parameter-sets: "Z0LAKNoB4AiflwFqAgQ... " 973 |             payload: 96 974 |                ssrc: 1156415737 975 |    timestamp-offset: 2282790541 976 |       seqnum-offset: 687 977 |         a-framerate: 30 978 | 979 | 980 | 981 | _proxypad2_0x555ad52a6520 982 | 983 | proxypad2 984 | [>][bfb] 985 | 986 | 987 | 988 | rtpbin_0x555ad5206050_send_rtcp_src_0_0x555ad52a4620 989 | 990 | send_rtcp_src_0 991 | [>][bfb] 992 | 993 | 994 | 995 | _proxypad2_0x555ad52a6520->rtpbin_0x555ad5206050_send_rtcp_src_0_0x555ad52a4620 996 | 997 | 998 | 999 | 1000 | 1001 | rtpbin_0x555ad5206050_send_rtcp_src_0_0x555ad52a4620->udpsink1_0x555ad526a560_sink_0x555ad5247850 1002 | 1003 | 1004 | application/x-rtcp 1005 | 1006 | 1007 | 1008 | _proxypad4_0x555ad52a69e0 1009 | 1010 | proxypad4 1011 | [>][bfb] 1012 | 1013 | 1014 | 1015 | rtpbin_0x555ad5206050_send_rtp_src_1_0x555ad52a4b20 1016 | 1017 | send_rtp_src_1 1018 | [>][bfb] 1019 | 1020 | 1021 | 1022 | _proxypad4_0x555ad52a69e0->rtpbin_0x555ad5206050_send_rtp_src_1_0x555ad52a4b20 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | rtpbin_0x555ad5206050_send_rtp_src_1_0x555ad52a4b20->udpsink2_0x555ad52881f0_sink_0x555ad5278ee0 1029 | 1030 | 1031 | application/x-rtp 1032 |               media: audio 1033 |          clock-rate: 48000 1034 |       encoding-name: OPUS 1035 |  sprop-maxcapturerate: 48000 1036 |        sprop-stereo: 0 1037 |             payload: 96 1038 |     encoding-params: 2 1039 |                ssrc: 4177561214 1040 |    timestamp-offset: 1368107043 1041 |       seqnum-offset: 27022 1042 | 1043 | 1044 | 1045 | _proxypad6_0x555ad52a6ea0 1046 | 1047 | proxypad6 1048 | [>][bfb] 1049 | 1050 | 1051 | 1052 | rtpbin_0x555ad5206050_send_rtcp_src_1_0x555ad52a5020 1053 | 1054 | send_rtcp_src_1 1055 | [>][bfb] 1056 | 1057 | 1058 | 1059 | _proxypad6_0x555ad52a6ea0->rtpbin_0x555ad5206050_send_rtcp_src_1_0x555ad52a5020 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | rtpbin_0x555ad5206050_send_rtcp_src_1_0x555ad52a5020->udpsink3_0x555ad52899f0_sink_0x555ad5279130 1066 | 1067 | 1068 | application/x-rtcp 1069 | 1070 | 1071 | 1072 | rtpstorage1_0x555ad5261360_sink_0x555ad529fd20 1073 | 1074 | sink 1075 | [>][bfb] 1076 | 1077 | 1078 | 1079 | rtpstorage1_0x555ad5261360_src_0x555ad529fad0 1080 | 1081 | src 1082 | [>][bfb] 1083 | 1084 | 1085 | 1086 | 1087 | rtpssrcdemux1_0x555ad5133c00_sink_0x555ad529f630 1088 | 1089 | sink 1090 | [>][bfb] 1091 | 1092 | 1093 | 1094 | rtpssrcdemux1_0x555ad5133c00_rtcp_sink_0x555ad529f880 1095 | 1096 | rtcp_sink 1097 | [>][bfb] 1098 | 1099 | 1100 | 1101 | rtpsession1_0x555ad529a510_send_rtp_src_0x555ad52b0430 1102 | 1103 | send_rtp_src 1104 | [>][bfb] 1105 | 1106 | 1107 | 1108 | 1109 | rtpsession1_0x555ad529a510_send_rtp_src_0x555ad52b0430->_proxypad4_0x555ad52a69e0 1110 | 1111 | 1112 | application/x-rtp 1113 |               media: audio 1114 |          clock-rate: 48000 1115 |       encoding-name: OPUS 1116 |  sprop-maxcapturerate: 48000 1117 |        sprop-stereo: 0 1118 |             payload: 96 1119 |     encoding-params: 2 1120 |                ssrc: 4177561214 1121 |    timestamp-offset: 1368107043 1122 |       seqnum-offset: 27022 1123 | 1124 | 1125 | 1126 | rtpsession1_0x555ad529a510_send_rtcp_src_0x555ad52b0680 1127 | 1128 | send_rtcp_src 1129 | [>][bfb] 1130 | 1131 | 1132 | 1133 | rtpsession1_0x555ad529a510_send_rtcp_src_0x555ad52b0680->_proxypad6_0x555ad52a6ea0 1134 | 1135 | 1136 | application/x-rtcp 1137 | 1138 | 1139 | 1140 | rtpsession1_0x555ad529a510_sync_src_0x555ad52b0b20 1141 | 1142 | sync_src 1143 | [>][bfb] 1144 | 1145 | 1146 | 1147 | rtpsession1_0x555ad529a510_sync_src_0x555ad52b0b20->rtpssrcdemux1_0x555ad5133c00_rtcp_sink_0x555ad529f880 1148 | 1149 | 1150 | application/x-rtcp 1151 | 1152 | 1153 | 1154 | rtpstorage0_0x555ad5261240_sink_0x555ad529e3b0 1155 | 1156 | sink 1157 | [>][bfb] 1158 | 1159 | 1160 | 1161 | rtpstorage0_0x555ad5261240_src_0x555ad529e160 1162 | 1163 | src 1164 | [>][bfb] 1165 | 1166 | 1167 | 1168 | 1169 | rtpssrcdemux0_0x555ad5133ad0_sink_0x555ad5279a70 1170 | 1171 | sink 1172 | [>][bfb] 1173 | 1174 | 1175 | 1176 | rtpssrcdemux0_0x555ad5133ad0_rtcp_sink_0x555ad5279cc0 1177 | 1178 | rtcp_sink 1179 | [>][bfb] 1180 | 1181 | 1182 | 1183 | rtpsession0_0x555ad529a260_send_rtp_src_0x555ad529e850 1184 | 1185 | send_rtp_src 1186 | [>][bfb] 1187 | 1188 | 1189 | 1190 | 1191 | rtpsession0_0x555ad529a260_send_rtp_src_0x555ad529e850->_proxypad0_0x555ad52a6060 1192 | 1193 | 1194 | application/x-rtp 1195 |               media: video 1196 |          clock-rate: 90000 1197 |       encoding-name: H264 1198 |  packetization-mode: 1 1199 |    profile-level-id: 42c028 1200 |  sprop-parameter-sets: "Z0LAKNoB4AiflwFqAgQ... " 1201 |             payload: 96 1202 |                ssrc: 1156415737 1203 |    timestamp-offset: 2282790541 1204 |       seqnum-offset: 687 1205 |         a-framerate: 30 1206 | 1207 | 1208 | 1209 | rtpsession0_0x555ad529a260_send_rtcp_src_0x555ad529eaa0 1210 | 1211 | send_rtcp_src 1212 | [>][bfb] 1213 | 1214 | 1215 | 1216 | rtpsession0_0x555ad529a260_send_rtcp_src_0x555ad529eaa0->_proxypad2_0x555ad52a6520 1217 | 1218 | 1219 | application/x-rtcp 1220 | 1221 | 1222 | 1223 | rtpsession0_0x555ad529a260_sync_src_0x555ad529ef40 1224 | 1225 | sync_src 1226 | [>][bfb] 1227 | 1228 | 1229 | 1230 | rtpsession0_0x555ad529a260_sync_src_0x555ad529ef40->rtpssrcdemux0_0x555ad5133ad0_rtcp_sink_0x555ad5279cc0 1231 | 1232 | 1233 | application/x-rtcp 1234 | 1235 | 1236 | 1237 | -------------------------------------------------------------------------------- /setup.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | // Copyright 2020 Google LLC 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // https://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "log" 22 | "os" 23 | "os/exec" 24 | ) 25 | 26 | func must(name string, args ...string) { 27 | c := exec.Command(name, args...) 28 | c.Stdin = os.Stdin 29 | c.Stdout = os.Stdout 30 | c.Stderr = os.Stderr 31 | log.Println(c.Args) 32 | if err := c.Run(); err != nil { 33 | log.Fatalf("%v: %v", c.Args, err) 34 | } 35 | } 36 | 37 | func setup() error { 38 | var ( 39 | restart = flag.Bool("restart", false, "whether to try to remove the kernel modules first") 40 | ) 41 | flag.Parse() 42 | 43 | // TODO(safety): check if snd-aloop or v4l2loopback are already loaded and 44 | // ask for confirmation by way of passing -restart (or manually unloading 45 | // the modules) 46 | 47 | if *restart { 48 | must("pulseaudio", "-k") 49 | must("sudo", "rmmod", "snd-aloop", "v4l2loopback") 50 | } 51 | 52 | must("sudo", "modprobe", "v4l2loopback", "video_nr=10,11", "card_label=v4l2-topeer,v4l2-frompeer", "exclusive_caps=1") 53 | 54 | // TODO: to get rid of the snd-aloop kernel module altogether, 55 | // I tried using a PulseAudio null sink, but OBS would not list 56 | // it as an option for the audio monitor device. 57 | 58 | // TODO: consider using pcm_substreams=1. will that effectively get rid of 59 | // the un-intuitive cross-connection? 60 | // - tried it and it behaved weirdly 61 | must("sudo", "modprobe", "snd-aloop", "enable=1", "index=10", "id=aloop-topeer") 62 | 63 | must("pacmd", "update-source-proplist alsa_input.platform-snd_aloop.0.analog-stereo device.description=\"aloop-topeer source\"") 64 | must("pacmd", "update-sink-proplist alsa_output.platform-snd_aloop.0.analog-stereo device.description=\"aloop-topeer sink\"") 65 | 66 | return nil 67 | } 68 | 69 | func main() { 70 | if err := setup(); err != nil { 71 | log.Fatal(err) 72 | } 73 | } 74 | --------------------------------------------------------------------------------