Merge pull request #248 from vexorian/20210123_dev
#18 Allowing to play audio files in channels.
This commit is contained in:
commit
5650d07a54
4
index.js
4
index.js
@ -212,6 +212,10 @@ function initDB(db, channelDB) {
|
||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-offline-screen.png')))
|
||||
fs.writeFileSync(process.env.DATABASE + '/images/generic-offline-screen.png', data)
|
||||
}
|
||||
if (!fs.existsSync(process.env.DATABASE + '/images/generic-music-screen.png')) {
|
||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-music-screen.png')))
|
||||
fs.writeFileSync(process.env.DATABASE + '/images/generic-music-screen.png', data)
|
||||
}
|
||||
if (!fs.existsSync(process.env.DATABASE + '/images/loading-screen.png')) {
|
||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
|
||||
fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
|
||||
|
||||
BIN
resources/generic-music-screen.png
Normal file
BIN
resources/generic-music-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@ -111,6 +111,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
let ffmpegArgs = [
|
||||
`-threads`, isConcatPlaylist? 1 : this.opts.threads,
|
||||
`-fflags`, `+genpts+discardcorrupt+igndts`];
|
||||
let stillImage = false;
|
||||
|
||||
if (
|
||||
(limitRead === true)
|
||||
@ -185,28 +186,57 @@ class FFMPEG extends events.EventEmitter {
|
||||
}
|
||||
|
||||
// prepare input streams
|
||||
if ( typeof(streamUrl.errorTitle) !== 'undefined') {
|
||||
if ( ( typeof(streamUrl.errorTitle) !== 'undefined') || (streamStats.audioOnly) ) {
|
||||
doOverlay = false; //never show icon in the error screen
|
||||
// for error stream, we have to generate the input as well
|
||||
this.apad = false; //all of these generate audio correctly-aligned to video so there is no need for apad
|
||||
this.audioChannelsSampleRate = true; //we'll need these
|
||||
|
||||
if (this.ensureResolution) {
|
||||
//all of the error strings already choose the resolution to
|
||||
//match iW x iH , so with this we save ourselves a second
|
||||
// scale filter
|
||||
iW = this.wantedW;
|
||||
iH = this.wantedH;
|
||||
//all of the error strings already choose the resolution to
|
||||
//match iW x iH , so with this we save ourselves a second
|
||||
// scale filter
|
||||
iW = this.wantedW;
|
||||
iH = this.wantedH;
|
||||
|
||||
if (this.audioOnly !== true) {
|
||||
ffmpegArgs.push("-r" , "24");
|
||||
let pic = null;
|
||||
|
||||
//does an image to play exist?
|
||||
if (
|
||||
(typeof(streamUrl.errorTitle) === 'undefined')
|
||||
&&
|
||||
(streamStats.audioOnly)
|
||||
) {
|
||||
pic = streamStats.placeholderImage;
|
||||
} else if ( streamUrl.errorTitle == 'offline') {
|
||||
pic = `${this.channel.offlinePicture}`;
|
||||
} else if ( this.opts.errorScreen == 'pic' ) {
|
||||
pic = `${this.errorPicturePath}`;
|
||||
}
|
||||
|
||||
if ( this.audioOnly !== true) {
|
||||
ffmpegArgs.push("-r" , "24");
|
||||
if ( streamUrl.errorTitle == 'offline' ) {
|
||||
if (pic != null) {
|
||||
ffmpegArgs.push(
|
||||
'-loop', '1',
|
||||
'-i', `${this.channel.offlinePicture}`,
|
||||
'-i', pic,
|
||||
);
|
||||
videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped];[looped]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
|
||||
if (
|
||||
(typeof duration === 'undefined')
|
||||
&&
|
||||
(typeof(streamStats.duration) !== 'undefined' )
|
||||
) {
|
||||
//add 150 milliseconds just in case, exact duration seems to cut out the last bits of music some times.
|
||||
duration = `${streamStats.duration + 150}ms`;
|
||||
}
|
||||
videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped]`;
|
||||
videoComplex += `;[looped]format=yuv420p[formatted]`;
|
||||
let stream = "scaled";
|
||||
videoComplex +=`;[formatted]scale=w=${iW}:h=${iH}:force_original_aspect_ratio=1[scaled]`;
|
||||
if (this.ensureResolution) {
|
||||
stream = "padded";
|
||||
videoComplex += `;[scaled]pad=${iW}:${iH}:(ow-iw)/2:(oh-ih)/2[padded]`;
|
||||
}
|
||||
videoComplex +=`;[${stream}]realtime[videox]`;
|
||||
stillImage = true;
|
||||
} else if (this.opts.errorScreen == 'static') {
|
||||
ffmpegArgs.push(
|
||||
'-f', 'lavfi',
|
||||
@ -232,23 +262,17 @@ class FFMPEG extends events.EventEmitter {
|
||||
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') {
|
||||
} else { //blank
|
||||
ffmpegArgs.push(
|
||||
'-f', 'lavfi',
|
||||
'-i', `color=c=black:s=${iW}x${iH}`
|
||||
);
|
||||
inputFiles++;
|
||||
videoComplex = `;realtime[videox]`;
|
||||
} else {//'pic'
|
||||
ffmpegArgs.push(
|
||||
'-loop', '1',
|
||||
'-i', `${this.errorPicturePath}`,
|
||||
);
|
||||
inputFiles++;
|
||||
videoComplex = `;[${videoFile+1}:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`;
|
||||
}
|
||||
}
|
||||
let durstr = `duration=${streamStats.duration}ms`;
|
||||
if (typeof(streamUrl.errorTitle) !== 'undefined') {
|
||||
//silent
|
||||
audioComplex = `;aevalsrc=0:${durstr}[audioy]`;
|
||||
if ( streamUrl.errorTitle == 'offline' ) {
|
||||
@ -280,8 +304,9 @@ class FFMPEG extends events.EventEmitter {
|
||||
ffmpegArgs.push('-pix_fmt' , 'yuv420p' );
|
||||
}
|
||||
audioComplex += ';[audioy]arealtime[audiox]';
|
||||
currentVideo = "[videox]";
|
||||
currentAudio = "[audiox]";
|
||||
}
|
||||
currentVideo = "[videox]";
|
||||
}
|
||||
if (doOverlay) {
|
||||
if (watermark.animated === true) {
|
||||
@ -297,9 +322,13 @@ class FFMPEG extends events.EventEmitter {
|
||||
let algo = this.opts.scalingAlgorithm;
|
||||
let resizeMsg = "";
|
||||
if (
|
||||
(!streamStats.audioOnly)
|
||||
&&
|
||||
(
|
||||
(this.ensureResolution && ( streamStats.anamorphic || (iW != this.wantedW || iH != this.wantedH) ) )
|
||||
||
|
||||
isLargerResolution(iW, iH, this.wantedW, this.wantedH)
|
||||
)
|
||||
) {
|
||||
//scaler stuff, need to change the size of the video and also add bars
|
||||
// calculate wanted aspect ratio
|
||||
@ -444,6 +473,9 @@ class FFMPEG extends events.EventEmitter {
|
||||
`-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
|
||||
`-sc_threshold`, `1000000000`,
|
||||
);
|
||||
if (stillImage) {
|
||||
ffmpegArgs.push('-tune', 'stillimage');
|
||||
}
|
||||
}
|
||||
ffmpegArgs.push(
|
||||
'-map', currentAudio,
|
||||
@ -506,14 +538,14 @@ class FFMPEG extends events.EventEmitter {
|
||||
`service_provider="dizqueTV"`,
|
||||
`-metadata`,
|
||||
`service_name="${this.channel.name}"`,
|
||||
`-f`, `mpegts`);
|
||||
);
|
||||
|
||||
//t should be before output
|
||||
//t should be before -f
|
||||
if (typeof duration !== 'undefined') {
|
||||
ffmpegArgs.push(`-t`, duration)
|
||||
ffmpegArgs.push(`-t`, `${duration}`);
|
||||
}
|
||||
|
||||
ffmpegArgs.push(`pipe:1`)
|
||||
ffmpegArgs.push(`-f`, `mpegts`, `pipe:1`)
|
||||
|
||||
let doLogs = this.opts.logFfmpeg && !isConcatPlaylist;
|
||||
if (this.hasBeenKilled) {
|
||||
@ -521,6 +553,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
}
|
||||
this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } );
|
||||
if (this.hasBeenKilled) {
|
||||
console.log("Send SIGKILL to ffmpeg");
|
||||
this.ffmpeg.kill("SIGKILL");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -124,11 +124,7 @@ class PlexPlayer {
|
||||
return emitter;
|
||||
|
||||
} catch(err) {
|
||||
if (err instanceof Error) {
|
||||
throw err;
|
||||
} else {
|
||||
return Error("Error when playing plex program: " + JSON.stringify(err) );
|
||||
}
|
||||
return Error("Error when playing plex program: " + JSON.stringify(err) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,10 @@ class PlexTranscoder {
|
||||
this.updateInterval = 30000
|
||||
this.updatingPlex = undefined
|
||||
this.playState = "stopped"
|
||||
this.albumArt = {
|
||||
attempted : false,
|
||||
path: null,
|
||||
}
|
||||
}
|
||||
|
||||
async getStream(deinterlace) {
|
||||
@ -53,7 +57,7 @@ class PlexTranscoder {
|
||||
} else {
|
||||
try {
|
||||
this.log("Setting transcoding parameters")
|
||||
this.setTranscodingArgs(stream.directPlay, true, deinterlace)
|
||||
this.setTranscodingArgs(stream.directPlay, true, deinterlace, true)
|
||||
await this.getDecision(stream.directPlay);
|
||||
if (this.isDirectPlay()) {
|
||||
stream.directPlay = true;
|
||||
@ -110,13 +114,14 @@ class PlexTranscoder {
|
||||
return stream
|
||||
}
|
||||
|
||||
setTranscodingArgs(directPlay, directStream, deinterlace) {
|
||||
setTranscodingArgs(directPlay, directStream, deinterlace, firstTry) {
|
||||
let resolution = (directStream) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution
|
||||
let bitrate = (directStream) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate
|
||||
let mediaBufferSize = (directStream) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize
|
||||
let subtitles = (this.settings.enableSubtitles) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar
|
||||
let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing
|
||||
let isDirectPlay = (directPlay) ? '1' : '0'
|
||||
let isDirectPlay = (directPlay) ? '1' : (firstTry? '': '0');
|
||||
let hasMDE = '1';
|
||||
|
||||
let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set
|
||||
let profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra
|
||||
@ -166,7 +171,7 @@ X-Plex-Token=${this.server.accessToken}&\
|
||||
X-Plex-Client-Profile-Extra=${clientProfile_enc}&\
|
||||
protocol=${this.settings.streamProtocol}&\
|
||||
Connection=keep-alive&\
|
||||
hasMDE=1&\
|
||||
hasMDE=${hasMDE}&\
|
||||
path=${this.key}&\
|
||||
mediaIndex=0&\
|
||||
partIndex=0&\
|
||||
@ -206,6 +211,9 @@ lang=en`
|
||||
|
||||
isDirectPlay() {
|
||||
try {
|
||||
if (this.getVideoStats().audioOnly) {
|
||||
return this.getVideoStats().audioDecision === "copy";
|
||||
}
|
||||
return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy";
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" , e);
|
||||
@ -217,7 +225,6 @@ lang=en`
|
||||
let ret = {}
|
||||
try {
|
||||
let streams = this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].Stream
|
||||
|
||||
ret.duration = parseFloat( this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].duration );
|
||||
streams.forEach(function (_stream, $index) {
|
||||
// Video
|
||||
@ -257,6 +264,14 @@ lang=en`
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" , e);
|
||||
}
|
||||
if (typeof(ret.videoCodec) === 'undefined') {
|
||||
ret.audioOnly = true;
|
||||
ret.placeholderImage = (this.albumArt.path != null) ?
|
||||
ret.placeholderImage = this.albumArt.path
|
||||
:
|
||||
ret.placeholderImage = `http://localhost:${process.env.PORT}/images/generic-music-screen.png`
|
||||
;
|
||||
}
|
||||
|
||||
this.log("Current video stats:")
|
||||
this.log(ret)
|
||||
@ -300,23 +315,56 @@ lang=en`
|
||||
}
|
||||
|
||||
async getDecisionUnmanaged(directPlay) {
|
||||
let res = await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, {
|
||||
let url = `${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`;
|
||||
let res = await axios.get(url, {
|
||||
headers: { Accept: 'application/json' }
|
||||
})
|
||||
this.decisionJson = res.data;
|
||||
|
||||
this.log("Recieved transcode decision:")
|
||||
this.log("Received transcode decision:");
|
||||
this.log(res.data)
|
||||
|
||||
// Print error message if transcode not possible
|
||||
// TODO: handle failure better
|
||||
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode
|
||||
if (!(directPlay || transcodeDecisionCode == "1001")) {
|
||||
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode;
|
||||
if (
|
||||
( typeof(transcodeDecisionCode) === 'undefined' )
|
||||
) {
|
||||
this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo';
|
||||
console.log("Audio-only file detected");
|
||||
await this.tryToGetAlbumArt();
|
||||
} else if (!(directPlay || transcodeDecisionCode == "1001")) {
|
||||
console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
|
||||
console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
|
||||
}
|
||||
}
|
||||
|
||||
async tryToGetAlbumArt() {
|
||||
try {
|
||||
if(this.albumArt.attempted ) {
|
||||
return;
|
||||
}
|
||||
this.albumArt.attempted = true;
|
||||
|
||||
this.log("Try to get album art:");
|
||||
let url = `${this.server.uri}${this.key}?${this.transcodingArgs}`;
|
||||
let res = await axios.get(url, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
let mediaContainer = res.data.MediaContainer;
|
||||
if (typeof(mediaContainer) !== 'undefined') {
|
||||
for( let i = 0; i < mediaContainer.Metadata.length; i++) {
|
||||
console.log("got art: " + mediaContainer.Metadata[i].thumb );
|
||||
this.albumArt.path = `${this.server.uri}${mediaContainer.Metadata[i].thumb}?${this.transcodingArgs}`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error when getting album art", err);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async getDecision(directPlay) {
|
||||
try {
|
||||
await this.getDecisionUnmanaged(directPlay);
|
||||
|
||||
115
src/svg/generic-music-screen.svg
Normal file
115
src/svg/generic-music-screen.svg
Normal file
@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1920"
|
||||
height="1080"
|
||||
viewBox="0 0 507.99999 285.75001"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="generic-music-screen.svg"
|
||||
inkscape:export-filename="/home/vx/dev/pseudotv/resources/generic-music-screen.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="4.4585776"
|
||||
inkscape:cx="925.75604"
|
||||
inkscape:cy="448.17449"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Capa 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-11.249983)">
|
||||
<rect
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.20000029;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect836"
|
||||
width="508"
|
||||
height="285.75"
|
||||
x="0"
|
||||
y="11.249983" />
|
||||
<g
|
||||
id="g6050"
|
||||
transform="translate(-8.4960767,30.053154)">
|
||||
<rect
|
||||
transform="rotate(0.52601418)"
|
||||
y="85.000603"
|
||||
x="214.56714"
|
||||
height="73.832573"
|
||||
width="32.814484"
|
||||
id="rect4518"
|
||||
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<rect
|
||||
transform="rotate(1.4727575)"
|
||||
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4520"
|
||||
width="32.81448"
|
||||
height="73.832573"
|
||||
x="248.74632"
|
||||
y="80.901688" />
|
||||
<rect
|
||||
transform="rotate(-3.2986121)"
|
||||
y="103.78287"
|
||||
x="269.35843"
|
||||
height="73.832588"
|
||||
width="32.814476"
|
||||
id="rect4522"
|
||||
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:76.95687866px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="147.14322"
|
||||
y="234.94209"
|
||||
id="text838"
|
||||
transform="scale(1.3642872,0.73298349)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan836"
|
||||
x="147.14322"
|
||||
y="234.94209"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:KacstPoster;-inkscape-font-specification:KacstPoster;stroke-width:0.26458335px">♪</tspan></text>
|
||||
<ellipse
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.52916664;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path840"
|
||||
cx="240.60326"
|
||||
cy="169.0907"
|
||||
rx="15.090722"
|
||||
ry="15.089045" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
Loading…
x
Reference in New Issue
Block a user