diff --git a/index.js b/index.js
index 36ea844..13af0b0 100644
--- a/index.js
+++ b/index.js
@@ -187,7 +187,11 @@ app.listen(process.env.PORT, () => {
})
function initDB(db, channelDB) {
- dbMigration.initDB(db, channelDB);
+ if (!fs.existsSync(process.env.DATABASE + '/images/dizquetv.png')) {
+ let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/dizquetv.png')))
+ fs.writeFileSync(process.env.DATABASE + '/images/dizquetv.png', data)
+ }
+ dbMigration.initDB(db, channelDB, __dirname);
if (!fs.existsSync(process.env.DATABASE + '/font.ttf')) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/font.ttf')))
fs.writeFileSync(process.env.DATABASE + '/font.ttf', data)
diff --git a/resources/dizquetv.png b/resources/dizquetv.png
index bac3553..f3c0eec 100644
Binary files a/resources/dizquetv.png and b/resources/dizquetv.png differ
diff --git a/resources/favicon.svg b/resources/favicon.svg
index 679b6ea..8de1542 100644
--- a/resources/favicon.svg
+++ b/resources/favicon.svg
@@ -29,7 +29,7 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.0547013"
- inkscape:cx="173.01248"
+ inkscape:cx="55.816079"
inkscape:cy="84.726326"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
@@ -48,7 +48,7 @@
image/svg+xml
-
+
@@ -58,7 +58,7 @@
id="layer1"
transform="translate(0,-244.08278)">
+ style="opacity:1;fill:#080808;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
addFPS(db) ],
[ 601, 700, (db) => migrateWatermark(db) ],
[ 700, 701, (db) => addScalingAlgorithm(db) ],
- [ 701, 702, (db) => addDeinterlaceFilter(db) ]
+ [ 701, 703, (db,channels,dir) => reAddIcon(dir) ],
+ [ 703, 800, (db) => addDeinterlaceFilter(db) ],
+ // there was a bit of thing in which for a while 1.3.x migrated 701 to 702 using
+ // the addDeinterlaceFilter step. This 702 step no longer exists as a target
+ // but we have to migrate it to 800 using the reAddIcon.
+ [ 702, 800, (db,channels,dir) => reAddIcon(dir) ],
]
const { v4: uuidv4 } = require('uuid');
@@ -333,7 +338,7 @@ function commercialsRemover(db) {
}
-function initDB(db, channelDB ) {
+function initDB(db, channelDB, dir ) {
if (typeof(channelDB) === 'undefined') {
throw Error("???");
}
@@ -348,7 +353,7 @@ function initDB(db, channelDB ) {
ran = true;
console.log("Migrating from db version " + dbVersion.version + " to: " + STEPS[i][1] + "...");
try {
- STEPS[i][2](db, channelDB);
+ STEPS[i][2](db, channelDB, dir);
if (typeof(dbVersion._id) === 'undefined') {
db['db-version'].save( {'version': STEPS[i][1] } );
} else {
@@ -398,7 +403,7 @@ function ffmpeg() {
normalizeAudio: true,
maxFPS: 60,
scalingAlgorithm: "bicubic",
- deinterlaceFilter: "none"
+ deinterlaceFilter: "none",
}
}
@@ -757,6 +762,40 @@ function addScalingAlgorithm(db) {
fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) );
}
+function moveBackup(path) {
+ if (fs.existsSync(`${process.env.DATABASE}${path}`) ) {
+ let i = 0;
+ while (fs.existsSync( `${process.env.DATABASE}${path}.bak.${i}`) ) {
+ i++;
+ }
+ fs.renameSync(`${process.env.DATABASE}${path}`, `${process.env.DATABASE}${path}.bak.${i}` );
+ }
+}
+
+function reAddIcon(dir) {
+ moveBackup('/images/dizquetv.png');
+ let data = fs.readFileSync(path.resolve(path.join(dir, 'resources/dizquetv.png')));
+ fs.writeFileSync(process.env.DATABASE + '/images/dizquetv.png', data);
+
+ if (fs.existsSync(`${process.env.DATABASE}/images/pseudotv.png`) ) {
+ moveBackup('/images/pseudotv.png');
+ let data = fs.readFileSync(path.resolve(path.join(dir, 'resources/dizquetv.png')));
+ fs.writeFileSync(process.env.DATABASE + '/images/pseudotv.png', data);
+ }
+
+ moveBackup('/images/generic-error-screen.png');
+ data = fs.readFileSync(path.resolve(path.join(dir, 'resources/generic-error-screen.png')))
+ fs.writeFileSync(process.env.DATABASE + '/images/generic-error-screen.png', data)
+
+ moveBackup('/images/generic-offline-screen.png');
+ data = fs.readFileSync(path.resolve(path.join(dir, 'resources/generic-offline-screen.png')));
+ fs.writeFileSync(process.env.DATABASE + '/images/generic-offline-screen.png', data);
+
+ moveBackup('/images/loading-screen.png');
+ data = fs.readFileSync(path.resolve(path.join(dir, 'resources/loading-screen.png')))
+ fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
+}
+
function addDeinterlaceFilter(db) {
let ffmpegSettings = db['ffmpeg-settings'].find()[0];
let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json');
diff --git a/src/ffmpeg.js b/src/ffmpeg.js
index f2403df..8fcd3cd 100644
--- a/src/ffmpeg.js
+++ b/src/ffmpeg.js
@@ -62,6 +62,10 @@ class FFMPEG extends events.EventEmitter {
this.ensureResolution = this.opts.normalizeResolution;
this.volumePercent = this.opts.audioVolumePercent;
this.hasBeenKilled = false;
+ this.audioOnly = false;
+ }
+ setAudioOnly(audioOnly) {
+ this.audioOnly = audioOnly;
}
async spawnConcat(streamUrl) {
return await this.spawn(streamUrl, undefined, undefined, undefined, true, false, undefined, true)
@@ -108,8 +112,17 @@ class FFMPEG extends events.EventEmitter {
`-threads`, isConcatPlaylist? 1 : this.opts.threads,
`-fflags`, `+genpts+discardcorrupt+igndts`];
- if (limitRead === true)
- ffmpegArgs.push(`-re`)
+ if (
+ (limitRead === true)
+ &&
+ (
+ (this.audioOnly !== true)
+ ||
+ ( typeof(streamUrl.errorTitle) === 'undefined')
+ )
+ ) {
+ ffmpegArgs.push(`-re`);
+ }
if (typeof startTime !== 'undefined')
@@ -186,24 +199,27 @@ class FFMPEG extends events.EventEmitter {
iH = this.wantedH;
}
+ if ( this.audioOnly !== true) {
ffmpegArgs.push("-r" , "24");
if ( streamUrl.errorTitle == 'offline' ) {
ffmpegArgs.push(
'-loop', '1',
'-i', `${this.channel.offlinePicture}`,
);
- videoComplex = `;[0:0]loop=loop=-1:size=1:start=0[looped];[looped]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
+ videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped];[looped]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
} else if (this.opts.errorScreen == 'static') {
ffmpegArgs.push(
'-f', 'lavfi',
'-i', `nullsrc=s=64x36`);
videoComplex = `;geq=random(1)*255:128:128[videoz];[videoz]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
+ inputFiles++;
} else if (this.opts.errorScreen == 'testsrc') {
ffmpegArgs.push(
'-f', 'lavfi',
'-i', `testsrc=size=${iW}x${iH}`,
);
videoComplex = `;realtime[videox]`;
+ inputFiles++;
} else if (this.opts.errorScreen == 'text') {
var sz2 = Math.ceil( (iH) / 33.0);
var sz1 = Math.ceil( sz2 * 3. / 2. );
@@ -213,6 +229,7 @@ class FFMPEG extends events.EventEmitter {
'-f', 'lavfi',
'-i', `color=c=black:s=${iW}x${iH}`
);
+ inputFiles++;
videoComplex = `;drawtext=fontfile=${process.env.DATABASE}/font.ttf:fontsize=${sz1}:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text='${streamUrl.errorTitle}',drawtext=fontfile=${process.env.DATABASE}/font.ttf:fontsize=${sz2}:fontcolor=white:x=(w-text_w)/2:y=(h+text_h+${sz3})/2:text='${streamUrl.subtitle}'[videoy];[videoy]realtime[videox]`;
} else if (this.opts.errorScreen == 'blank') {
@@ -220,14 +237,17 @@ class FFMPEG extends events.EventEmitter {
'-f', 'lavfi',
'-i', `color=c=black:s=${iW}x${iH}`
);
+ inputFiles++;
videoComplex = `;realtime[videox]`;
} else {//'pic'
ffmpegArgs.push(
'-loop', '1',
'-i', `${this.errorPicturePath}`,
);
- videoComplex = `;[0:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
+ inputFiles++;
+ videoComplex = `;[${videoFile+1}:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
}
+ }
let durstr = `duration=${streamStats.duration}ms`;
//silent
audioComplex = `;aevalsrc=0:${durstr}[audioy]`;
@@ -239,14 +259,26 @@ class FFMPEG extends events.EventEmitter {
ffmpegArgs.push('-i', `${this.channel.offlineSoundtrack}`);
// I don't really understand why, but you need to use this
// 'size' in order to make the soundtrack actually loop
- audioComplex = `;[1:a]aloop=loop=-1:size=2147483647[audioy]`;
+ audioComplex = `;[${inputFiles++}:a]aloop=loop=-1:size=2147483647[audioy]`;
}
- } else if (this.opts.errorAudio == 'whitenoise') {
- audioComplex = `;aevalsrc=-2+0.1*random(0):${durstr}[audioy]`;
+ } else if (
+ (this.opts.errorAudio == 'whitenoise')
+ ||
+ (
+ !(this.opts.errorAudio == 'sine')
+ &&
+ (this.audioOnly === true) //when it's in audio-only mode, silent stream is confusing for errors.
+ )
+ ) {
+ audioComplex = `;aevalsrc=random(0):${durstr}[audioy]`;
+ this.volumePercent = Math.min(70, this.volumePercent);
} else if (this.opts.errorAudio == 'sine') {
- audioComplex = `;sine=f=440:${durstr}[audiox];[audiox]volume=-35dB[audioy]`;
+ audioComplex = `;sine=f=440:${durstr}[audioy]`;
+ this.volumePercent = Math.min(70, this.volumePercent);
+ }
+ if ( this.audioOnly !== true ) {
+ ffmpegArgs.push('-pix_fmt' , 'yuv420p' );
}
- ffmpegArgs.push('-pix_fmt' , 'yuv420p' );
audioComplex += ';[audioy]arealtime[audiox]';
currentVideo = "[videox]";
currentAudio = "[audiox]";
@@ -319,7 +351,7 @@ class FFMPEG extends events.EventEmitter {
}
// Channel watermark:
- if (doOverlay) {
+ if (doOverlay && (this.audioOnly !== true) ) {
var pW =watermark.width;
var w = Math.round( pW * iW / 100.0 );
var mpHorz = watermark.horizontalMargin;
@@ -361,7 +393,8 @@ class FFMPEG extends events.EventEmitter {
currentAudio = '[boosted]';
}
// Align audio is just the apad filter applied to audio stream
- if (this.apad) {
+ if (this.apad && (this.audioOnly !== true) ) {
+ //it doesn't make much sense to pad audio when there is no video
audioComplex += `;${currentAudio}apad=whole_dur=${streamStats.duration}ms[padded]`;
currentAudio = '[padded]';
} else if (this.audioChannelsSampleRate) {
@@ -382,11 +415,13 @@ class FFMPEG extends events.EventEmitter {
} else {
console.log(resizeMsg)
}
- if (currentVideo != '[video]') {
- transcodeVideo = true; //this is useful so that it adds some lines below
- filterComplex += videoComplex;
- } else {
- currentVideo = `${videoFile}:${videoIndex}`;
+ if (this.audioOnly !== true) {
+ if (currentVideo != '[video]') {
+ transcodeVideo = true; //this is useful so that it adds some lines below
+ filterComplex += videoComplex;
+ } else {
+ currentVideo = `${videoFile}:${videoIndex}`;
+ }
}
// same with audio:
if (currentAudio != '[audio]') {
@@ -403,15 +438,18 @@ class FFMPEG extends events.EventEmitter {
ffmpegArgs.push('-shortest');
}
}
-
+ if (this.audioOnly !== true) {
+ ffmpegArgs.push(
+ '-map', currentVideo,
+ `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
+ `-sc_threshold`, `1000000000`,
+ );
+ }
ffmpegArgs.push(
- '-map', currentVideo,
'-map', currentAudio,
- `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
`-flags`, `cgop+ilme`,
- `-sc_threshold`, `1000000000`
);
- if ( transcodeVideo ) {
+ if ( transcodeVideo && (this.audioOnly !== true) ) {
// add the video encoder flags
ffmpegArgs.push(
`-b:v`, `${this.opts.videoBitrate}k`,
@@ -453,8 +491,11 @@ class FFMPEG extends events.EventEmitter {
//Concat stream is simpler and should always copy the codec
ffmpegArgs.push(
`-probesize`, 32 /*`100000000`*/,
- `-i`, streamUrl,
- `-map`, `0:v`,
+ `-i`, streamUrl );
+ if (this.audioOnly !== true) {
+ ffmpegArgs.push( `-map`, `0:v` );
+ }
+ ffmpegArgs.push(
`-map`, `0:${audioIndex}`,
`-c`, `copy`,
`-muxdelay`, this.opts.concatMuxDelay,
diff --git a/src/offline-player.js b/src/offline-player.js
index 4cd0361..38d846e 100644
--- a/src/offline-player.js
+++ b/src/offline-player.js
@@ -19,6 +19,7 @@ class OfflinePlayer {
context.channel.offlineSoundtrack = undefined;
}
this.ffmpeg = new FFMPEG(context.ffmpegSettings, context.channel);
+ this.ffmpeg.setAudioOnly(this.context.audioOnly);
}
cleanUp() {
@@ -37,7 +38,7 @@ class OfflinePlayer {
} else {
ff = await ffmpeg.spawnOffline(duration);
}
- ff.pipe(outStream);
+ ff.pipe(outStream, {'end':false} );
ffmpeg.on('end', () => {
emitter.emit('end');
@@ -45,8 +46,33 @@ class OfflinePlayer {
ffmpeg.on('close', () => {
emitter.emit('close');
});
- ffmpeg.on('error', (err) => {
- emitter.emit('error', err);
+ ffmpeg.on('error', async (err) => {
+ //wish this code wasn't repeated.
+ if (! this.error ) {
+ console.log("Replacing failed stream with error stream");
+ ff.unpipe(outStream);
+ ffmpeg.removeAllListeners('data');
+ ffmpeg.removeAllListeners('end');
+ ffmpeg.removeAllListeners('error');
+ ffmpeg.removeAllListeners('close');
+ ffmpeg = new FFMPEG(this.context.ffmpegSettings, this.context.channel); // Set the transcoder options
+ ffmpeg.setAudioOnly(this.context.audioOnly);
+ ffmpeg.on('close', () => {
+ emitter.emit('close');
+ });
+ ffmpeg.on('end', () => {
+ emitter.emit('end');
+ });
+ ffmpeg.on('error', (err) => {
+ emitter.emit('error', err );
+ });
+
+ ff = await ffmpeg.spawnError('oops', 'oops', Math.min(duration, 60000) );
+ ff.pipe(outStream);
+ } else {
+ emitter.emit('error', err);
+ }
+
});
return emitter;
} catch(err) {
diff --git a/src/plex-player.js b/src/plex-player.js
index 9bde669..4874bbd 100644
--- a/src/plex-player.js
+++ b/src/plex-player.js
@@ -62,6 +62,7 @@ class PlexPlayer {
this.plexTranscoder = plexTranscoder;
let watermark = this.context.watermark;
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
+ ffmpeg.setAudioOnly( this.context.audioOnly );
this.ffmpeg = ffmpeg;
let streamDuration;
if (typeof(lineupItem.streamDuration)!=='undefined') {
@@ -97,13 +98,14 @@ class PlexPlayer {
emitter.emit('close');
});
ffmpeg.on('error', async (err) => {
- console.log("Replacing failed stream with error streram");
+ console.log("Replacing failed stream with error stream");
ff.unpipe(outStream);
ffmpeg.removeAllListeners('data');
ffmpeg.removeAllListeners('end');
ffmpeg.removeAllListeners('error');
ffmpeg.removeAllListeners('close');
ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
+ ffmpeg.setAudioOnly(this.context.audioOnly);
ffmpeg.on('close', () => {
emitter.emit('close');
});
diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js
index ca40040..8106572 100644
--- a/src/plexTranscoder.js
+++ b/src/plexTranscoder.js
@@ -1,5 +1,6 @@
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
+const fs = require('fs');
class PlexTranscoder {
constructor(clientId, server, settings, channel, lineupItem) {
@@ -73,6 +74,14 @@ class PlexTranscoder {
// Update transcode decision for session
await this.getDecision(stream.directPlay);
stream.streamUrl = (this.settings.streamPath === 'direct') ? this.file : this.plexFile;
+ if(this.settings.streamPath === 'direct') {
+ fs.access(this.file, fs.F_OK, (err) => {
+ if (err) {
+ throw Error("Can't access this file", err);
+ return
+ }
+ })
+ }
if (typeof(stream.streamUrl) == 'undefined') {
throw Error("Direct path playback is not possible for this program because it was registered at a time when the direct path settings were not set. To fix this, you must either revert the direct path setting or rebuild this channel.");
}
@@ -291,10 +300,6 @@ lang=en`
}
async getDecisionUnmanaged(directPlay) {
- if (this.settings.streamPath === 'direct') {
- console.log("Skip get transcode decision because direct path is enabled");
- return;
- }
let res = await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, {
headers: { Accept: 'application/json' }
})
diff --git a/src/svg/dizquetv.svg b/src/svg/dizquetv.svg
index 4224dbe..074a661 100644
--- a/src/svg/dizquetv.svg
+++ b/src/svg/dizquetv.svg
@@ -16,9 +16,9 @@
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="dizquetv.svg"
- inkscape:export-filename="/home/vx/dev/dizqueanimation/01.png"
- inkscape:export-xdpi="245.75999"
- inkscape:export-ydpi="245.75999">
+ inkscape:export-filename="/home/vx/dev/pseudotv/resources/dizquetv.png"
+ inkscape:export-xdpi="240"
+ inkscape:export-ydpi="240">
image/svg+xml
-
+
@@ -58,7 +58,7 @@
id="layer1"
transform="translate(0,-244.08278)">
+ style="opacity:1;fill:#080808;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
{
+ let concat = async (req, res, audioOnly) => {
// Check if channel queried is valid
if (typeof req.query.channel === 'undefined') {
res.status(500).send("No Channel Specified")
@@ -75,6 +75,7 @@ function video( channelDB , fillerDB, db) {
console.log(`\r\nStream starting. Channel: ${channel.number} (${channel.name})`)
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
+ ffmpeg.setAudioOnly(audioOnly);
let stopped = false;
function stop() {
@@ -109,9 +110,16 @@ function video( channelDB , fillerDB, db) {
})
let channelNum = parseInt(req.query.channel, 10)
- let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}`);
+ let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}`);
ff.pipe(res );
- })
+ };
+ router.get('/video', async(req, res) => {
+ return await concat(req, res, false);
+ } );
+ router.get('/radio', async(req, res) => {
+ return await concat(req, res, true);
+ } );
+
// Stream individual video to ffmpeg concat above. This is used by the server, NOT the client
router.get('/stream', async (req, res) => {
// Check if channel queried is valid
@@ -119,6 +127,8 @@ function video( channelDB , fillerDB, db) {
res.status(400).send("No Channel Specified")
return
}
+ let audioOnly = ("true" == req.query.audioOnly);
+ console.log(`/stream audioOnly=${audioOnly}`);
let session = parseInt(req.query.session);
let m3u8 = (req.query.m3u8 === '1');
let number = parseInt(req.query.channel);
@@ -296,6 +306,7 @@ function video( channelDB , fillerDB, db) {
channel: combinedChannel,
db: db,
m3u8: m3u8,
+ audioOnly : audioOnly,
}
let player = new ProgramPlayer(playerContext);
@@ -416,6 +427,7 @@ function video( channelDB , fillerDB, db) {
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
let sessionId = StreamCount++;
+ let audioOnly = ("true" == req.query.audioOnly);
if (
(ffmpegSettings.enableFFMPEGTranscoding === true)
@@ -423,12 +435,14 @@ function video( channelDB , fillerDB, db) {
&& (ffmpegSettings.normalizeAudioCodec === true)
&& (ffmpegSettings.normalizeResolution === true)
&& (ffmpegSettings.normalizeAudio === true)
+ && (audioOnly !== true) /* loading screen is pointless in audio mode (also for some reason it makes it fail when codec is aac, and I can't figure out why) */
) {
- data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}'\n`;
+ //loading screen
+ data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}&audioOnly=${audioOnly}'\n`;
}
- data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}'\n`
+ data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) {
- data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}'\n`
+ data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}&audioOnly=${audioOnly}'\n`
}
res.send(data)
diff --git a/web/public/index.html b/web/public/index.html
index a495695..3b20003 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -13,7 +13,9 @@