├── .gitignore ├── .github └── img │ ├── outputPage.png │ ├── consoleOutput.png │ └── streamSettings.png ├── config.json ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.github/img/outputPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Der/obs-into-discord/HEAD/.github/img/outputPage.png -------------------------------------------------------------------------------- /.github/img/consoleOutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Der/obs-into-discord/HEAD/.github/img/consoleOutput.png -------------------------------------------------------------------------------- /.github/img/streamSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sean-Der/obs-into-discord/HEAD/.github/img/streamSettings.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "userToken": "", 3 | "serverIdNumber": "", 4 | "channelIdNumber": "", 5 | "httpPort": 4321 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@dank074/discord-video-stream": "^3.2.0", 4 | "discord.js-selfbot-v13": "^3.3.0", 5 | "werift": "^0.19.6" 6 | }, 7 | "devDependencies": { 8 | "standard": "^17.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sean DuBois 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `obs-into-discord` has been discontinued; use [`utsuru`](https://github.com/VincentVerdynanta/utsuru) instead. 2 | 3 | ------ 4 | 5 | # OBS Into Discord 6 | 7 | [![License][license-image]][license-url] 8 | [![Discord][discord-image]][discord-invite-url] 9 | 10 | - [What is OBS Into Discord](#what-is-obs-into-discord) 11 | - [Setup](#setup) 12 | - [Using](#using) 13 | - [TODO](#todo) 14 | - [More](#more) 15 | 16 | ## What is OBS Into Discord 17 | 18 | This project allows you to send video from OBS directly into Discord. 19 | The video is not transcoded or modified in any way. What you send is 20 | then sent directly to discord. 21 | 22 | As a user this gives you 23 | * Higher quality video. Tune your encoding settings! 24 | * Better platform support. Get audio for Linux screenshares. 25 | * More customization. Custom layouts/capture multiple windows.... 26 | 27 | This projects accepts WebRTC (WHIP) clients using [werift-webrtc](https://github.com/shinyoshiaki/werift-webrtc). It then bridges this 28 | into Discord using [dank074/Discord-video-stream](https://github.com/dank074/Discord-video-stream). This project also supports more then 29 | just OBS. You can also use any client that support WHIP. 30 | 31 | * [GStreamer](https://gstreamer.freedesktop.org/documentation/webrtchttp/whipsink.html?gi-language=c) 32 | * [Larix Broadcaster](https://softvelum.com/larix/) 33 | * [FFmpeg](https://github.com/ossrs/ffmpeg-webrtc) 34 | * [Web Browser](https://github.com/Eyevinn/whip) 35 | 36 | 37 | ## Setup 38 | 39 | ### Download 40 | 41 | `git clone https://github.com/Sean-Der/obs-into-discord.git` 42 | 43 | ### Install Dependencies 44 | 45 | `cd obs-into-discord && npm install` 46 | 47 | ### Set `userToken` in `config.json` 48 | 49 | Getting the `userToken` is the hardest step. I found these guides the most useful 50 | 51 | * https://gist.github.com/MarvNC/e601f3603df22f36ebd3102c501116c6 52 | * https://www.geeksforgeeks.org/how-to-get-discord-token/ 53 | 54 | ### Set `serverIdNumber` and `channelIdNumber` 55 | 56 | Follow the official documentation [here](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID) 57 | 58 | ### Change `httpPort` if 4321 isn't available 59 | 60 | In most cases you will not to change this. Only ff port 4321 is already used by another service. 61 | 62 | ## Using 63 | 64 | Run `node index.js`. If it starts without errors you have configured everything correctly. You now have a WHIP -> Discord bridge 65 | running on port 4321. Publish from OBS via WHIP to `http://localhost:4321`. Currently you can do any streamKey (this may change in the future) 66 | 67 | ![OBS Stream settings example](./.github/img/streamSettings.png) 68 | 69 | The following encoding settings are recommended. You can try tuning these, but may 70 | see inconsistent playbcak behavior 71 | 72 | ![OBS Output settings example](./.github/img/outputPage.png) 73 | 74 | If everything worked you should see the following output in the console. 75 | 76 | ![Console output](./.github/img/consoleOutput.png) 77 | 78 | ## TODO 79 | 80 | * [ ] Allow configuration via Web UI 81 | * [ ] Creater Docker Image 82 | * [ ] Upload to Dockerhub 83 | * [ ] Create [executable](https://nodejs.org/api/single-executable-applications.html) for easier install 84 | * [ ] Support H265 85 | * [ ] Support AV1 86 | * [ ] Lower Latency 87 | 88 | ## More 89 | 90 | For self hosting see [Broadcast Box](https://github.com/glimesh/broadcast-box). With Broadcast Box you can broadcast 91 | with 150Ms of latency and push any video quality you want. No bitrate or codec restrictions! 92 | 93 | [Join the Discord][discord-invite-url] and we are ready to help! 94 | 95 | [license-image]: https://img.shields.io/badge/License-MIT-yellow.svg 96 | [license-url]: https://opensource.org/licenses/MIT 97 | [discord-image]: https://img.shields.io/discord/1162823780708651018?logo=discord 98 | [discord-invite-url]: https://discord.gg/An5jjhNUE3 99 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | 3 | const { RTCPeerConnection, RTCRtpCodecParameters, H264RtpPayload } = require('werift') 4 | const { Client } = require('discord.js-selfbot-v13') 5 | const { Streamer, H264NalSplitter } = require('@dank074/discord-video-stream') 6 | 7 | const redText = '\x1b[31m' 8 | const greenText = '\x1b[32m' 9 | 10 | const config = require('./config.json') 11 | checkConfig() 12 | 13 | http.createServer(async (req, res) => { 14 | if (req.method !== 'POST') { 15 | return res.end() 16 | } 17 | 18 | console.log(`${greenText}New WHIP Session`) 19 | 20 | const streamer = new Streamer(new Client()) 21 | 22 | try { 23 | await streamer.client.login(config.userToken) 24 | } catch (err) { 25 | console.log(`${redText}Failed to login, check userToken: ${err.toString()}`) 26 | return res.end() 27 | } 28 | 29 | try { 30 | await streamer.joinVoice(config.serverIdNumber, config.channelIdNumber, {}) 31 | } catch (err) { 32 | console.log(`${redText}Failed to joinVoice, check serverIdNumber and channelIdNumber: ${err.toString()}`) 33 | return res.end() 34 | } 35 | 36 | const udp = await streamer.createStream({}) 37 | 38 | udp.mediaConnection.setSpeaking(true) 39 | udp.mediaConnection.setVideoStatus(true) 40 | 41 | const nalSplitter = new H264NalSplitter() 42 | nalSplitter.on('data', frame => { 43 | udp.sendVideoFrame(frame) 44 | }) 45 | 46 | let h264Res = new H264RtpPayload() 47 | handleWHIPRequest(req, res, 48 | connectionState => { 49 | console.log(`${greenText}WHIP Session state change ${connectionState}`) 50 | 51 | if (connectionState === 'disconnected') { 52 | streamer.stopStream() 53 | streamer.leaveVoice() 54 | } 55 | }, 56 | audioPacket => { 57 | udp.sendAudioFrame(audioPacket.payload) 58 | }, 59 | videoPacket => { 60 | h264Res = H264RtpPayload.deSerialize(videoPacket.payload, h264Res.fragment) 61 | if (h264Res.payload !== undefined) { 62 | nalSplitter._transform(h264Res.payload, null, () => {}) 63 | } 64 | }) 65 | }) 66 | .on('error', err => { 67 | console.log(`${redText}Failed to start HTTP server: ${err.toString()}`) 68 | }) 69 | .listen(config.httpPort) 70 | 71 | function handleWHIPRequest (req, res, onConnectionState, onAudio, onVideo) { 72 | let body = '' 73 | req.on('data', chunk => { 74 | body += chunk 75 | }) 76 | 77 | req.on('end', () => { 78 | const pc = new RTCPeerConnection({ 79 | iceServers: [], 80 | codecs: { 81 | audio: [ 82 | new RTCRtpCodecParameters({ 83 | mimeType: 'audio/opus', 84 | clockRate: 48000, 85 | channels: 2 86 | }) 87 | ], 88 | video: [ 89 | new RTCRtpCodecParameters({ 90 | mimeType: 'video/H264', 91 | clockRate: 90000, 92 | rtcpFeedback: [ 93 | { type: 'nack' }, 94 | { type: 'nack', parameter: 'pli' }, 95 | { type: 'goog-remb' } 96 | ], 97 | parameters: 98 | 'profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1' 99 | }) 100 | ] 101 | } 102 | }) 103 | 104 | pc.iceConnectionStateChange.subscribe(onConnectionState) 105 | 106 | pc.addTransceiver('video', { direction: 'recvonly' }).onTrack.subscribe( 107 | (track) => 108 | track.onReceiveRtp.subscribe((packet) => { 109 | onVideo(packet) 110 | }) 111 | ) 112 | 113 | pc.addTransceiver('audio', { direction: 'recvonly' }).onTrack.subscribe( 114 | (track) => 115 | track.onReceiveRtp.subscribe((packet) => { 116 | onAudio(packet) 117 | }) 118 | ) 119 | 120 | pc.setRemoteDescription({ 121 | type: 'offer', 122 | sdp: body 123 | }) 124 | 125 | pc.createAnswer().then(answer => { 126 | pc.setLocalDescription(answer) 127 | 128 | let timerStarted = false 129 | pc.onicecandidate = ({ candidate }) => { 130 | if (timerStarted) { 131 | return 132 | } 133 | timerStarted = true 134 | 135 | setTimeout(() => { 136 | res.setHeader('Location', '/') 137 | res.statusCode = 201 138 | res.end(pc.localDescription.sdp) 139 | }, 200) 140 | } 141 | }) 142 | }) 143 | } 144 | 145 | function checkConfig () { 146 | const startupErrors = [] 147 | if (config.userToken === '') { 148 | startupErrors.push(`${redText}Config is missing userToken`) 149 | } 150 | if (config.serverIdNumber === '') { 151 | startupErrors.push(`${redText}Config is missing serverIdNumber`) 152 | } 153 | if (config.channelIdNumber === '') { 154 | startupErrors.push(`${redText}Config is missing channelIdNumber`) 155 | } 156 | if (!config.httpPort) { 157 | startupErrors.push(`${redText}Config is missing httpPort`) 158 | } 159 | 160 | startupErrors.forEach((e, i) => { 161 | console.log(e) 162 | }) 163 | 164 | if (startupErrors.length !== 0) { 165 | process.exit(1) 166 | } 167 | } 168 | --------------------------------------------------------------------------------