Agregar configuración de pantalla dividida: posición y estiramiento

This commit is contained in:
Cesar Mendivil 2026-02-15 18:58:25 -07:00
parent 244b956936
commit 6fc05fab1d
6 changed files with 94 additions and 13 deletions

View File

@ -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) {

View File

@ -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,
};
}

View File

@ -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')

View File

@ -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 = () => {

View File

@ -789,6 +789,19 @@
<br></br>
<label>Secondary Width (%)</label>
<input type="number" class="form-control form-control-sm" ng-model="channel.splitScreen.widthPercent"></input>
<small class='form-text text-muted'>Percentage of width/height depending on position.</small>
<br></br>
<label>Position</label>
<select class="form-control form-control-sm" ng-model="channel.splitScreen.position">
<option value="right">Right</option>
<option value="left">Left</option>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
</select>
<br></br>
<input id="channelSplitStretch" type="checkbox" ng-model="channel.splitScreen.stretch"></input>
<label for="channelSplitStretch">Stretch secondary (fill entire area)</label>
<small class='form-text text-muted'>If unchecked, maintains aspect ratio with padding.</small>
<br></br>
<input id="channelSplitLoop" type="checkbox" ng-model="channel.splitScreen.loop"></input>
<label for="channelSplitLoop">Loop secondary source</label>

View File

@ -270,6 +270,19 @@
<br></br>
<label>Secondary Width (%)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.splitScreenWidthPercent"></input>
<small class="form-text text-muted">Percentage of width/height depending on position.</small>
<br></br>
<label>Position</label>
<select class="form-control form-control-sm" ng-model="settings.splitScreenPosition">
<option value="right">Right</option>
<option value="left">Left</option>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
</select>
<br></br>
<input id="splitScreenStretch" type="checkbox" ng-model="settings.splitScreenStretch"></input>
<label for="splitScreenStretch">Stretch secondary (fill entire area)</label>
<small class="form-text text-muted">If unchecked, maintains aspect ratio with padding.</small>
<br></br>
<input id="splitScreenLoop" type="checkbox" ng-model="settings.splitScreenLoop"></input>
<label for="splitScreenLoop">Loop secondary source</label>