diff --git a/examples/VoiceChannel/PlayVideo.js b/examples/VoiceChannel/PlayVideo.js index 0fb80ef..bb16bcb 100644 --- a/examples/VoiceChannel/PlayVideo.js +++ b/examples/VoiceChannel/PlayVideo.js @@ -46,7 +46,9 @@ client.on('ready', async client => { const input = 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'; // Play const dispatcher = stream.playVideo(input, { - fps: 30, + fps: 60, + bitrate: 4000, + resolution: '1080', }); const dispatcher2 = stream.playAudio(input); dispatcher.on('start', () => { diff --git a/src/client/voice/dispatcher/AnnexBDispatcher.js b/src/client/voice/dispatcher/AnnexBDispatcher.js index dc4f16f..011a649 100644 --- a/src/client/voice/dispatcher/AnnexBDispatcher.js +++ b/src/client/voice/dispatcher/AnnexBDispatcher.js @@ -29,7 +29,6 @@ class AnnexBDispatcher extends VideoDispatcher { codecCallback(frame) { let accessUnit = frame; - const nalus = []; let offset = 0; // Extract NALUs from the access unit @@ -37,25 +36,19 @@ class AnnexBDispatcher extends VideoDispatcher { const naluSize = accessUnit.readUInt32BE(offset); offset += 4; const nalu = accessUnit.subarray(offset, offset + naluSize); - nalus.push(nalu); - offset += naluSize; - } - - nalus.forEach((nalu, index) => { - const isLastNal = index === nalus.length - 1; - + const isLastNal = offset + naluSize >= accessUnit.length; if (nalu.length <= this.mtu) { // If NALU size is within MTU, send it directly - this._playChunk(Buffer.concat([this.createHeaderExtension(), nalu]), index + 1 === nalus.length); + this._playChunk(Buffer.concat([this.createHeaderExtension(), nalu]), isLastNal); } else { // If NALU size exceeds MTU, fragment it const [naluHeader, naluData] = this._nalFunctions.splitHeader(nalu); const dataFragments = this.partitionVideoData(naluData); - dataFragments.forEach((data, fragmentIndex) => { + for (let fragmentIndex = 0; fragmentIndex < dataFragments.length; fragmentIndex++) { + const data = dataFragments[fragmentIndex]; const isFirstPacket = fragmentIndex === 0; const isFinalPacket = fragmentIndex === dataFragments.length - 1; - const markerBit = isLastNal && isFinalPacket; // Is last packet ? this._playChunk( Buffer.concat([ @@ -63,11 +56,12 @@ class AnnexBDispatcher extends VideoDispatcher { this.makeFragmentationUnitHeader(isFirstPacket, isFinalPacket, naluHeader), data, ]), - markerBit, + isLastNal && isFinalPacket, ); - }); + } } - }); + offset += naluSize; + } } } diff --git a/src/client/voice/dispatcher/BaseDispatcher.js b/src/client/voice/dispatcher/BaseDispatcher.js index f1b8bb3..962ef4f 100644 --- a/src/client/voice/dispatcher/BaseDispatcher.js +++ b/src/client/voice/dispatcher/BaseDispatcher.js @@ -316,12 +316,12 @@ class BaseDispatcher extends Writable { _createPacket(buffer, isLastPacket = false) { // Header const packetBuffer = Buffer.alloc(12); - packetBuffer[0] = (2 << 6) | ((this.extensionEnabled ? 1 : 0) << 4); + packetBuffer[0] = this.extensionEnabled ? 0x90 : 0x80; // 0b10000000 | ((this.extensionEnabled ? 1 : 0) << 4); packetBuffer[1] = this.payloadType; if (this.extensionEnabled) { if (isLastPacket) { - packetBuffer[1] |= 0b10000000; + packetBuffer[1] |= 0x80; } } diff --git a/src/client/voice/dispatcher/VPxDispatcher.js b/src/client/voice/dispatcher/VPxDispatcher.js index d5c2759..6a4d3de 100644 --- a/src/client/voice/dispatcher/VPxDispatcher.js +++ b/src/client/voice/dispatcher/VPxDispatcher.js @@ -25,27 +25,24 @@ class VP8Dispatcher extends VideoDispatcher { super(player, highWaterMark, streams, fps); } - makeChunk(buffer, i) { + makeChunk(buffer, isFirstFrame) { // Make frame const headerExtensionBuf = this.createHeaderExtension(); // Vp8 payload descriptor const payloadDescriptorBuf = Buffer.alloc(2); - payloadDescriptorBuf[0] = 0x80; + payloadDescriptorBuf[0] = isFirstFrame ? 0x90 : 0x80; // Mark S bit, indicates start of frame: payloadDescriptorBuf[0] |= 0b00010000; payloadDescriptorBuf[1] = 0x80; - if (i == 0) { - payloadDescriptorBuf[0] |= 0b00010000; // Mark S bit, indicates start of frame - } // Vp8 pictureid payload extension const pictureIdBuf = Buffer.alloc(2); - pictureIdBuf.writeUIntBE(this.count, 0, 2); - pictureIdBuf[0] |= 0b10000000; + pictureIdBuf.writeUintBE(this.count, 0, 2); + pictureIdBuf[0] |= 0x80; return Buffer.concat([headerExtensionBuf, payloadDescriptorBuf, pictureIdBuf, buffer]); } codecCallback(chunk) { const chunkSplit = this.partitionVideoData(chunk); for (let i = 0; i < chunkSplit.length; i++) { - this._playChunk(this.makeChunk(chunkSplit[i], i), i + 1 === chunkSplit.length); + this._playChunk(this.makeChunk(chunkSplit[i], i == 0), i + 1 === chunkSplit.length); } } } diff --git a/src/client/voice/player/MediaPlayer.js b/src/client/voice/player/MediaPlayer.js index 6dd4f80..90a3332 100644 --- a/src/client/voice/player/MediaPlayer.js +++ b/src/client/voice/player/MediaPlayer.js @@ -132,6 +132,14 @@ class MediaPlayer extends EventEmitter { `${options?.fps}`, ]; + if (options?.resolution && options?.resolution !== 'maximum') { + args.push('-vf', `scale=-1:${options.resolution}`); + } + + if (options?.bitrate && typeof options?.bitrate === 'number') { + args.push('-b:v', `${options?.bitrate}K`); + } + if (!isStream) { args[1] = input; } @@ -144,28 +152,28 @@ class MediaPlayer extends EventEmitter { // Get stream type if (this.voiceConnection.videoCodec == 'VP8') { - args.push('-f', 'ivf', '-deadline', 'realtime', '-c:v', options?.copy ? 'copy' : 'libvpx'); + args.push('-f', 'ivf', '-deadline', 'realtime', '-c:v', 'libvpx'); // Remove '-speed', '5' bc bad quality } if (this.voiceConnection.videoCodec == 'H264') { args.push( '-c:v', - options?.copy ? 'copy' : 'libx264', + 'libx264', '-f', 'h264', '-tune', 'zerolatency', - '-pix_fmt', - 'yuv420p', + // '-pix_fmt', + // 'yuv420p', '-preset', - options?.preset || 'faster', + options?.presetH26x || 'faster', '-profile:v', 'baseline', - '-g', - `${options?.fps}`, - '-x264-params', - `keyint=${options?.fps}:min-keyint=${options?.fps}`, + // '-g', + // `${options?.fps}`, + // '-x264-params', + // `keyint=${options?.fps}:min-keyint=${options?.fps}`, '-bf', '0', '-bsf:v', diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js index a2a4bbc..6e72e1c 100644 --- a/src/client/voice/util/PlayInterface.js +++ b/src/client/voice/util/PlayInterface.js @@ -79,17 +79,19 @@ class PlayInterface { * @typedef {Object} VideoOptions * @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams * @property {number} [fps=30] Video fps - * @property {boolean} [copy=false] Copy codec ? + * @property {'maximum' | '480' | '720' | '1080' | '1440'} [resolution='maximum'] Resoluion (Height) * @property {number} [highWaterMark=12] The maximum number of opus packets to make and store before they are * actually needed. See https://nodejs.org/en/docs/guides/backpressuring-in-streams/. Setting this value to * 1 means that changes in volume will be more instant. - * @property {'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'} [preset='veryfast'] ffmpeg preset + * @property {'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'} [presetH26x='veryfast'] ffmpeg preset h254 / h265 * @property {boolean} [hwAccel=false] Enables hardware accelerated video decoding. Enabling this option might result in an exception * being thrown by Ffmpeg process if your system does not support hardware acceleration * @property {string[]} [inputFFmpegArgs] input ffmpeg * Ex: ['-config1', 'value1', '-config2', 'value2'] * @property {string[]} [outputFFmpegArgs] output ffmpeg * Ex: ['-config1', 'value1', '-config2', 'value2'] + * @property {number|'auto'} [bitrate=2000] The bitrate (quality) of the video in kbps. + * If set to 'auto', ffmpeg will automatically select */ /** diff --git a/typings/index.d.ts b/typings/index.d.ts index ec37810..f7ed7f6 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -932,10 +932,11 @@ export interface VideoOptions { highWaterMark?: number; fps?: number; hwAccel?: boolean; - preset?: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'; - copy?: boolean; + presetH26x?: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'; inputFFmpegArgs?: string[]; outputFFmpegArgs?: string[]; + resolution?: 'maximum' | '480' | '720' | '1080' | '1440'; + bitrate?: number | 'auto'; } export class BaseDispatcher extends Writable {