diff --git a/src/ffmpeg.js b/src/ffmpeg.js index b728363..8117060 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -496,23 +496,58 @@ class FFMPEG extends events.EventEmitter { // Split-screen composition (independent of watermark) // If we have a split-screen secondary input configured, compose - // it to the right of the main video using hstack after scaling + // it with the main video based on position (right/left/top/bottom) if (typeof(this._splitOverlayFile) !== 'undefined' && this._splitScreenConfig && this.audioOnly !== true) { console.log("[DEBUG] Building split-screen composition..."); try { const splitPercent = Number(this._splitScreenConfig.widthPercent) || 35; - const sideW = Math.max(16, Math.round(this.wantedW * splitPercent / 100.0)); - const mainW = Math.max(16, this.wantedW - sideW); - console.log(`[DEBUG] Split-screen dimensions: main=${mainW}x${this.wantedH}, side=${sideW}x${this.wantedH}`); - // scale secondary to wanted height, preserving aspect - videoComplex += `;[${this._splitOverlayFile}:v]scale=${sideW}:${this.wantedH}:force_original_aspect_ratio=decrease[side_scaled]`; - // scale main (currentVideo) to remaining width and wanted height - videoComplex += `;${currentVideo}scale=${mainW}:${this.wantedH}:force_original_aspect_ratio=decrease[main_scaled]`; - // pad both to exact dimensions if necessary - videoComplex += `;[main_scaled]pad=${mainW}:${this.wantedH}:(ow-iw)/2:(oh-ih)/2[main_padded]`; - videoComplex += `;[side_scaled]pad=${sideW}:${this.wantedH}:(ow-iw)/2:(oh-ih)/2[side_padded]`; - // stack horizontally - videoComplex += `;[main_padded][side_padded]hstack=inputs=2[comb2]`; + const position = this._splitScreenConfig.position || 'right'; + const stretch = !!this._splitScreenConfig.stretch; + + let mainW, mainH, sideW, sideH; + let stackType; // 'hstack' or 'vstack' + let stackOrder; // 'main-first' or 'side-first' + + // Calculate dimensions based on position + if (position === 'right' || position === 'left') { + // Horizontal split + sideW = Math.max(16, Math.round(this.wantedW * splitPercent / 100.0)); + mainW = Math.max(16, this.wantedW - sideW); + mainH = this.wantedH; + sideH = this.wantedH; + stackType = 'hstack'; + stackOrder = (position === 'left') ? 'side-first' : 'main-first'; + } else { + // Vertical split (top or bottom) + sideH = Math.max(16, Math.round(this.wantedH * splitPercent / 100.0)); + mainH = Math.max(16, this.wantedH - sideH); + mainW = this.wantedW; + sideW = this.wantedW; + stackType = 'vstack'; + stackOrder = (position === 'top') ? 'side-first' : 'main-first'; + } + + console.log(`[DEBUG] Split-screen: position=${position}, stretch=${stretch}, main=${mainW}x${mainH}, side=${sideW}x${sideH}`); + + // Scale filters based on stretch mode + if (stretch) { + // Stretch mode: scale exactly to dimensions (no aspect ratio preservation) + videoComplex += `;[${this._splitOverlayFile}:v]scale=${sideW}:${sideH}[side_scaled]`; + videoComplex += `;${currentVideo}scale=${mainW}:${mainH}[main_scaled]`; + } else { + // Aspect ratio mode: preserve aspect ratio with padding + videoComplex += `;[${this._splitOverlayFile}:v]scale=${sideW}:${sideH}:force_original_aspect_ratio=decrease[side_prescale]`; + videoComplex += `;[side_prescale]pad=${sideW}:${sideH}:(ow-iw)/2:(oh-ih)/2[side_scaled]`; + videoComplex += `;${currentVideo}scale=${mainW}:${mainH}:force_original_aspect_ratio=decrease[main_prescale]`; + videoComplex += `;[main_prescale]pad=${mainW}:${mainH}:(ow-iw)/2:(oh-ih)/2[main_scaled]`; + } + + // Stack based on type and order + if (stackOrder === 'main-first') { + videoComplex += `;[main_scaled][side_scaled]${stackType}=inputs=2[comb2]`; + } else { + videoComplex += `;[side_scaled][main_scaled]${stackType}=inputs=2[comb2]`; + } currentVideo = '[comb2]'; console.log("[DEBUG] Split-screen composition added to filter_complex"); } catch (e) { diff --git a/src/helperFuncs.js b/src/helperFuncs.js index 3c6ab56..fcc7973 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -360,6 +360,8 @@ function getSplitScreen(ffmpegSettings, channel) { enabled: !!channel.splitScreen.enabled, source: (typeof channel.splitScreen.source === 'string') ? channel.splitScreen.source : '', widthPercent: Number(channel.splitScreen.widthPercent) || 35, + position: (typeof channel.splitScreen.position === 'string') ? channel.splitScreen.position : 'right', + stretch: !!channel.splitScreen.stretch, loop: !!channel.splitScreen.loop, }; } else { @@ -371,6 +373,8 @@ function getSplitScreen(ffmpegSettings, channel) { enabled: !!ffmpegSettings.splitScreenEnabled, source: (typeof ffmpegSettings.splitScreenSource === 'string') ? ffmpegSettings.splitScreenSource : '', widthPercent: Number(ffmpegSettings.splitScreenWidthPercent) || 35, + position: (typeof ffmpegSettings.splitScreenPosition === 'string') ? ffmpegSettings.splitScreenPosition : 'right', + stretch: !!ffmpegSettings.splitScreenStretch, loop: !!ffmpegSettings.splitScreenLoop, }; } diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 184e492..23b94a0 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -95,6 +95,8 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get enabled: false, source: "", widthPercent: 35, + position: 'right', + stretch: false, loop: true, } scope.channel.onDemand = { @@ -160,9 +162,17 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get enabled: false, source: "", widthPercent: 35, + position: 'right', + stretch: false, loop: true, } } + if (typeof(scope.channel.splitScreen.position) === 'undefined') { + scope.channel.splitScreen.position = 'right'; + } + if (typeof(scope.channel.splitScreen.stretch) === 'undefined') { + scope.channel.splitScreen.stretch = false; + } if ( (scope.channel.transcoding.targetResolution == null) || (typeof(scope.channel.transcoding.targetResolution) === 'undefined') diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index fd236f7..ae8632b 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -11,12 +11,16 @@ module.exports = function (dizquetv, resolutionOptions) { scope.settings = settings // ensure videoFlip default exists scope.settings.videoFlip = scope.settings.videoFlip || 'none'; + scope.settings.splitScreenPosition = scope.settings.splitScreenPosition || 'right'; + scope.settings.splitScreenStretch = scope.settings.splitScreenStretch || false; }) scope.updateSettings = (settings) => { delete scope.settingsError; dizquetv.updateFfmpegSettings(settings).then((_settings) => { scope.settings = _settings scope.settings.videoFlip = scope.settings.videoFlip || 'none'; + scope.settings.splitScreenPosition = scope.settings.splitScreenPosition || 'right'; + scope.settings.splitScreenStretch = scope.settings.splitScreenStretch || false; }).catch( (err) => { if ( typeof(err.data) === "string") { scope.settingsError = err.data; @@ -27,6 +31,8 @@ module.exports = function (dizquetv, resolutionOptions) { dizquetv.resetFfmpegSettings(settings).then((_settings) => { scope.settings = _settings scope.settings.videoFlip = scope.settings.videoFlip || 'none'; + scope.settings.splitScreenPosition = scope.settings.splitScreenPosition || 'right'; + scope.settings.splitScreenStretch = scope.settings.splitScreenStretch || false; }) } scope.isTranscodingNotNeeded = () => { diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index cd6e3a0..3872f20 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -789,6 +789,19 @@

+ Percentage of width/height depending on position. +

+ + +

+ + + If unchecked, maintains aspect ratio with padding.

diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 148cdae..d883f93 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -270,6 +270,19 @@

+ Percentage of width/height depending on position. +

+ + +

+ + + If unchecked, maintains aspect ratio with padding.