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.