Merge remote-tracking branch 'origin/dev/1.5.x' into edge
This commit is contained in:
commit
c360ddae05
46
.github/workflows/binaries-build.yaml
vendored
Normal file
46
.github/workflows/binaries-build.yaml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
name: Build Executables and Update Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
release-files:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build dist image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-builder
|
||||
load: true
|
||||
tags: builder
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Run dist docker
|
||||
run: |
|
||||
docker run -v ./dist:/home/node/app/dist builder sh make_dist.sh
|
||||
|
||||
|
||||
- name: Upload Files
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ inputs.release }}
|
||||
files: |
|
||||
./dist/dizquetv-win-x64.exe
|
||||
./dist/dizquetv-win-x86.exe
|
||||
./dist/dizquetv-linux-x64
|
||||
./dist/dizquetv-macos-x64
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
13
.github/workflows/development-binaries.yaml
vendored
Normal file
13
.github/workflows/development-binaries.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Development Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev/1.5.x
|
||||
|
||||
jobs:
|
||||
binaries:
|
||||
uses: ./.github/workflows/binaries-build.yaml
|
||||
with:
|
||||
release: development-binaries
|
||||
secrets: inherit
|
||||
13
.github/workflows/development-tag.yaml
vendored
Normal file
13
.github/workflows/development-tag.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Development Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev/1.5.x
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: ./.github/workflows/docker-build.yaml
|
||||
with:
|
||||
tag: development
|
||||
secrets: inherit
|
||||
43
.github/workflows/docker-build.yaml
vendored
Normal file
43
.github/workflows/docker-build.yaml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Docker Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: Default
|
||||
Dockerfile: Dockerfile
|
||||
suffix: ""
|
||||
- name: nvidia
|
||||
Dockerfile: Dockerfile-nvidia
|
||||
suffix: "-nvidia"
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: vexorian/dizquetv:${{ inputs.tag }}${{ matrix.suffix }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
13
.github/workflows/edge-tag.yaml
vendored
Normal file
13
.github/workflows/edge-tag.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Edge Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- edge
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: ./.github/workflows/docker-build.yaml
|
||||
with:
|
||||
tag: edge
|
||||
secrets: inherit
|
||||
13
.github/workflows/latest-tag.yaml
vendored
Normal file
13
.github/workflows/latest-tag.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Latest Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: ./.github/workflows/docker-build.yaml
|
||||
with:
|
||||
tag: latest
|
||||
secrets: inherit
|
||||
13
.github/workflows/named-tag.yaml
vendored
Normal file
13
.github/workflows/named-tag.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Named Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: ./.github/workflows/docker-build.yaml
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
13
.github/workflows/tag-binaries.yaml
vendored
Normal file
13
.github/workflows/tag-binaries.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: Release Binaries
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
binaries:
|
||||
uses: ./.github/workflows/binaries-build.yaml
|
||||
with:
|
||||
release: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no-install commitlint --edit ""
|
||||
@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
exec < /dev/tty && node_modules/.bin/cz --hook || true
|
||||
@ -1,6 +1,6 @@
|
||||
FROM node:12.18-alpine3.12
|
||||
WORKDIR /home/node/app
|
||||
COPY package*.json ./
|
||||
COPY package.json ./
|
||||
RUN npm install && npm install -g browserify nexe@3.3.7
|
||||
COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
|
||||
COPY . .
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# dizqueTV 1.5.0
|
||||
# dizqueTV 1.5.1
|
||||
  
|
||||
|
||||
Create live TV channel streams from media on your Plex servers.
|
||||
|
||||
19
index.js
19
index.js
@ -29,6 +29,7 @@ const EventService = require("./src/services/event-service");
|
||||
const OnDemandService = require("./src/services/on-demand-service");
|
||||
const ProgrammingService = require("./src/services/programming-service");
|
||||
const ActiveChannelService = require('./src/services/active-channel-service')
|
||||
const ProgramPlayTimeDB = require('./src/dao/program-play-time-db')
|
||||
|
||||
const onShutdown = require("node-graceful-shutdown").onShutdown;
|
||||
|
||||
@ -95,7 +96,19 @@ channelService = new ChannelService(channelDB);
|
||||
|
||||
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelService );
|
||||
customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') );
|
||||
let programPlayTimeDB = new ProgramPlayTimeDB( path.join(process.env.DATABASE, 'play-cache') );
|
||||
|
||||
async function initializeProgramPlayTimeDB() {
|
||||
try {
|
||||
let t0 = new Date().getTime();
|
||||
await programPlayTimeDB.load();
|
||||
let t1 = new Date().getTime();
|
||||
console.log(`Program Play Time Cache loaded in ${t1-t0} milliseconds.`);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
initializeProgramPlayTimeDB();
|
||||
|
||||
fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') );
|
||||
cacheImageService = new CacheImageService(db, fileCache);
|
||||
@ -270,7 +283,7 @@ app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.cs
|
||||
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService ))
|
||||
app.use('/api/cache/images', cacheImageService.apiRouters())
|
||||
|
||||
app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService ))
|
||||
app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ))
|
||||
app.use(hdhr.router)
|
||||
app.listen(process.env.PORT, () => {
|
||||
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)
|
||||
@ -309,6 +322,10 @@ function initDB(db, channelDB) {
|
||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
|
||||
fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
|
||||
}
|
||||
if (!fs.existsSync(process.env.DATABASE + '/images/black.png')) {
|
||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/black.png')))
|
||||
fs.writeFileSync(process.env.DATABASE + '/images/black.png', data)
|
||||
}
|
||||
if (!fs.existsSync( path.join(process.env.DATABASE, 'custom.css') )) {
|
||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources', 'default-custom.css')))
|
||||
fs.writeFileSync( path.join(process.env.DATABASE, 'custom.css'), data)
|
||||
|
||||
13
package.json
13
package.json
@ -11,8 +11,7 @@
|
||||
"dev-server": "nodemon index.js --ignore ./web/ --ignore ./db/ --ignore ./xmltv.xml",
|
||||
"compile": "babel index.js -d dist && babel src -d dist/src",
|
||||
"package": "sh ./make_dist.sh",
|
||||
"clean": "del-cli --force ./bin ./dist ./.dizquetv ./web/public/bundle.js",
|
||||
"prepare": "husky install"
|
||||
"clean": "del-cli --force ./bin ./dist ./.dizquetv ./web/public/bundle.js"
|
||||
},
|
||||
"author": "vexorian",
|
||||
"license": "Zlib",
|
||||
@ -35,9 +34,10 @@
|
||||
"ng-i18next": "^1.0.7",
|
||||
"node-graceful-shutdown": "1.1.0",
|
||||
"node-ssdp": "^4.0.0",
|
||||
"quickselect": "2.0.0",
|
||||
"random-js": "2.1.0",
|
||||
"request": "^2.88.2",
|
||||
"uuid": "^8.0.0",
|
||||
"uuid": "9.0.1",
|
||||
"xml-writer": "^1.7.0"
|
||||
},
|
||||
"bin": "dist/index.js",
|
||||
@ -50,9 +50,7 @@
|
||||
"@commitlint/config-conventional": "^12.1.4",
|
||||
"browserify": "^16.5.1",
|
||||
"copyfiles": "^2.2.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"del-cli": "^3.0.0",
|
||||
"husky": "^7.0.0",
|
||||
"nexe": "^3.3.7",
|
||||
"nodemon": "^2.0.3",
|
||||
"watchify": "^3.11.1"
|
||||
@ -61,10 +59,5 @@
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
resources/black.png
Normal file
BIN
resources/black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@ -1,7 +1,7 @@
|
||||
const SLACK = require('./constants').SLACK;
|
||||
|
||||
let cache = {};
|
||||
let programPlayTimeCache = {};
|
||||
|
||||
let fillerPlayTimeCache = {};
|
||||
let configCache = {};
|
||||
let numbers = null;
|
||||
@ -14,11 +14,9 @@ async function getChannelConfig(channelDB, channelId) {
|
||||
if (channel == null) {
|
||||
configCache[channelId] = [];
|
||||
} else {
|
||||
//console.log("channel=" + JSON.stringify(channel) );
|
||||
configCache[channelId] = [channel];
|
||||
}
|
||||
}
|
||||
//console.log("channel=" + JSON.stringify(configCache[channelId]).slice(0,200) );
|
||||
return configCache[channelId];
|
||||
}
|
||||
|
||||
@ -106,7 +104,7 @@ function getCurrentLineupItem(channelId, t1) {
|
||||
return lineupItem;
|
||||
}
|
||||
|
||||
function getKey(channelId, program) {
|
||||
function getProgramKey(program) {
|
||||
let serverKey = "!unknown!";
|
||||
if (typeof(program.serverKey) !== 'undefined') {
|
||||
if (typeof(program.serverKey) !== 'undefined') {
|
||||
@ -117,36 +115,37 @@ function getKey(channelId, program) {
|
||||
if (typeof(program.key) !== 'undefined') {
|
||||
programKey = program.key;
|
||||
}
|
||||
return channelId + "|" + serverKey + "|" + programKey;
|
||||
|
||||
return serverKey + "|" + programKey;
|
||||
}
|
||||
|
||||
|
||||
function getFillerKey(channelId, fillerId) {
|
||||
return channelId + "|" + fillerId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function recordProgramPlayTime(channelId, lineupItem, t0) {
|
||||
function recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0) {
|
||||
let remaining;
|
||||
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
|
||||
remaining = lineupItem.streamDuration;
|
||||
} else {
|
||||
remaining = lineupItem.duration - lineupItem.start;
|
||||
}
|
||||
programPlayTimeCache[ getKey(channelId, lineupItem) ] = t0 + remaining;
|
||||
setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t0 + remaining);
|
||||
if (typeof(lineupItem.fillerId) !== 'undefined') {
|
||||
fillerPlayTimeCache[ getFillerKey(channelId, lineupItem.fillerId) ] = t0 + remaining;
|
||||
}
|
||||
}
|
||||
|
||||
function getProgramLastPlayTime(channelId, program) {
|
||||
let v = programPlayTimeCache[ getKey(channelId, program) ];
|
||||
if (typeof(v) === 'undefined') {
|
||||
return 0;
|
||||
} else {
|
||||
return v;
|
||||
}
|
||||
function setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t) {
|
||||
let programKey = getProgramKey(lineupItem);
|
||||
programPlayTime.update(channelId, programKey, t);
|
||||
}
|
||||
|
||||
function getProgramLastPlayTime(programPlayTime, channelId, program) {
|
||||
let programKey = getProgramKey(program);
|
||||
return programPlayTime.getProgramLastPlayTime(channelId, programKey);
|
||||
}
|
||||
|
||||
function getFillerLastPlayTime(channelId, fillerId) {
|
||||
@ -158,8 +157,8 @@ function getFillerLastPlayTime(channelId, fillerId) {
|
||||
}
|
||||
}
|
||||
|
||||
function recordPlayback(channelId, t0, lineupItem) {
|
||||
recordProgramPlayTime(channelId, lineupItem, t0);
|
||||
function recordPlayback(programPlayTime, channelId, t0, lineupItem) {
|
||||
recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0);
|
||||
|
||||
cache[channelId] = {
|
||||
t0: t0,
|
||||
|
||||
@ -5,6 +5,13 @@ module.exports = {
|
||||
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
|
||||
TOO_FREQUENT: 1000,
|
||||
|
||||
// Duration of things like the loading screen and the interlude (the black
|
||||
// frame that appears between videos). The goal of these things is to
|
||||
// prevent the video from getting stuck on the last second, which looks bad
|
||||
// for some reason ~750 works well. I raised the fps to 60 and now 420 works
|
||||
// but I wish it was lower.
|
||||
GAP_DURATION: 10*42,
|
||||
|
||||
//when a channel is forcibly stopped due to an update, let's mark it as active
|
||||
// for a while during the transaction just in case.
|
||||
CHANNEL_STOP_SHIELD : 5000,
|
||||
@ -28,5 +35,5 @@ module.exports = {
|
||||
// staying active, it checks every 5 seconds
|
||||
PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000,
|
||||
|
||||
VERSION_NAME: "1.5.0"
|
||||
VERSION_NAME: "1.5.1"
|
||||
}
|
||||
|
||||
90
src/dao/program-play-time-db.js
Normal file
90
src/dao/program-play-time-db.js
Normal file
@ -0,0 +1,90 @@
|
||||
const path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
class ProgramPlayTimeDB {
|
||||
|
||||
constructor(dir) {
|
||||
this.dir = dir;
|
||||
this.programPlayTimeCache = {};
|
||||
}
|
||||
|
||||
|
||||
async load() {
|
||||
try {
|
||||
if (! (await fs.promises.stat(this.dir)).isDirectory()) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
let files = await fs.promises.readdir(this.dir);
|
||||
|
||||
let processSubFileName = async (fileName, subDir, subFileName) => {
|
||||
try {
|
||||
if (subFileName.endsWith(".json")) {
|
||||
let programKey64 = subFileName.substring(
|
||||
0,
|
||||
subFileName.length - 4
|
||||
);
|
||||
let programKey = Buffer.from(programKey64, 'base64')
|
||||
.toString('utf-8');
|
||||
|
||||
|
||||
let filePath = path.join(subDir, subFileName);
|
||||
let fileContent = await fs.promises.readFile(
|
||||
filePath, 'utf-8');
|
||||
let jsonData = JSON.parse(fileContent);
|
||||
let key = getKey(fileName, programKey);
|
||||
this.programPlayTimeCache[ key ] = jsonData["t"]
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`When processing ${subDir}/${subFileName}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
let processFileName = async(fileName) => {
|
||||
|
||||
try {
|
||||
const subDir = path.join(this.dir, fileName);
|
||||
let subFiles = await fs.promises.readdir( subDir );
|
||||
|
||||
await Promise.all( subFiles.map( async subFileName => {
|
||||
return processSubFileName(fileName, subDir, subFileName);
|
||||
}) );
|
||||
} catch (err) {
|
||||
console.log(`When processing ${subDir}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all( files.map(processFileName) );
|
||||
}
|
||||
|
||||
getProgramLastPlayTime(channelId, programKey) {
|
||||
let v = this.programPlayTimeCache[ getKey(channelId, programKey) ];
|
||||
if (typeof(v) === 'undefined') {
|
||||
v = 0;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
async update(channelId, programKey, t) {
|
||||
|
||||
let key = getKey(channelId, programKey);
|
||||
this.programPlayTimeCache[ key ] = t;
|
||||
|
||||
const channelDir = path.join(this.dir, `${channelId}`);
|
||||
await fs.promises.mkdir( channelDir, { recursive: true } );
|
||||
let key64 = Buffer.from(programKey, 'utf-8').toString('base64');
|
||||
let filepath = path.join(channelDir, `${key64}.json`);
|
||||
let data = {t:t};
|
||||
await fs.promises.writeFile(filepath, JSON.stringify(data), 'utf-8');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getKey(channelId, programKey) {
|
||||
return channelId + "|" + programKey;
|
||||
}
|
||||
|
||||
|
||||
module.exports = ProgramPlayTimeDB;
|
||||
@ -198,8 +198,9 @@ class FFMPEG extends events.EventEmitter {
|
||||
iW = this.wantedW;
|
||||
iH = this.wantedH;
|
||||
|
||||
let durstr = `duration=${streamStats.duration}ms`;
|
||||
|
||||
if (this.audioOnly !== true) {
|
||||
ffmpegArgs.push("-r" , "24");
|
||||
let pic = null;
|
||||
|
||||
//does an image to play exist?
|
||||
@ -216,6 +217,11 @@ class FFMPEG extends events.EventEmitter {
|
||||
}
|
||||
|
||||
if (pic != null) {
|
||||
if (this.opts.noRealTime === true) {
|
||||
ffmpegArgs.push("-r" , "60");
|
||||
} else {
|
||||
ffmpegArgs.push("-r" , "24");
|
||||
}
|
||||
ffmpegArgs.push(
|
||||
'-i', pic,
|
||||
);
|
||||
@ -230,11 +236,17 @@ class FFMPEG extends events.EventEmitter {
|
||||
videoComplex = `;[${inputFiles++}:0]format=yuv420p[formatted]`;
|
||||
videoComplex +=`;[formatted]scale=w=${iW}:h=${iH}:force_original_aspect_ratio=1[scaled]`;
|
||||
videoComplex += `;[scaled]pad=${iW}:${iH}:(ow-iw)/2:(oh-ih)/2[padded]`;
|
||||
videoComplex += `;[padded]loop=loop=-1:size=1:start=0[looped]`;
|
||||
videoComplex +=`;[looped]realtime[videox]`;
|
||||
videoComplex += `;[padded]loop=loop=-1:size=1:start=0`;
|
||||
if (this.opts.noRealTime !== true) {
|
||||
videoComplex +=`[looped];[looped]realtime[videox]`;
|
||||
} else {
|
||||
videoComplex +=`[videox]`
|
||||
}
|
||||
//this tune apparently makes the video compress better
|
||||
// when it is the same image
|
||||
stillImage = true;
|
||||
this.volumePercent = Math.min(70, this.volumePercent);
|
||||
|
||||
} else if (this.opts.errorScreen == 'static') {
|
||||
ffmpegArgs.push(
|
||||
'-f', 'lavfi',
|
||||
@ -269,7 +281,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
videoComplex = `;realtime[videox]`;
|
||||
}
|
||||
}
|
||||
let durstr = `duration=${streamStats.duration}ms`;
|
||||
|
||||
if (typeof(streamUrl.errorTitle) !== 'undefined') {
|
||||
//silent
|
||||
audioComplex = `;aevalsrc=0:${durstr}[audioy]`;
|
||||
@ -471,7 +483,8 @@ class FFMPEG extends events.EventEmitter {
|
||||
`-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
|
||||
`-sc_threshold`, `1000000000`,
|
||||
);
|
||||
if (stillImage) {
|
||||
// do not use -tune stillimage for nv
|
||||
if (stillImage && ! this.opts.videoEncoder.toLowerCase().includes("nv") ) {
|
||||
ffmpegArgs.push('-tune', 'stillimage');
|
||||
}
|
||||
}
|
||||
@ -482,7 +495,6 @@ class FFMPEG extends events.EventEmitter {
|
||||
if ( transcodeVideo && (this.audioOnly !== true) ) {
|
||||
// add the video encoder flags
|
||||
ffmpegArgs.push(
|
||||
`-b:v`, `${this.opts.videoBitrate}k`,
|
||||
`-maxrate:v`, `${this.opts.videoBitrate}k`,
|
||||
`-bufsize:v`, `${this.opts.videoBufSize}k`
|
||||
);
|
||||
@ -549,6 +561,7 @@ class FFMPEG extends events.EventEmitter {
|
||||
if (this.hasBeenKilled) {
|
||||
return ;
|
||||
}
|
||||
//console.log(this.ffmpegPath + " " + ffmpegArgs.join(" ") );
|
||||
this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } );
|
||||
if (this.hasBeenKilled) {
|
||||
console.log("Send SIGKILL to ffmpeg");
|
||||
|
||||
@ -6,8 +6,10 @@ module.exports = {
|
||||
}
|
||||
|
||||
let channelCache = require('./channel-cache');
|
||||
const INFINITE_TIME = new Date().getTime() + 10*365*24*60*60*1000; //10 years from the initialization of the server. I dunno, I just wanted it to be a high time without it stopping being human readable if converted to date.
|
||||
const SLACK = require('./constants').SLACK;
|
||||
const randomJS = require("random-js");
|
||||
const quickselect = require("quickselect");
|
||||
const Random = randomJS.Random;
|
||||
const random = new Random( randomJS.MersenneTwister19937.autoSeed() );
|
||||
|
||||
@ -61,7 +63,7 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
|
||||
return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
|
||||
}
|
||||
|
||||
function createLineup(obj, channel, fillers, isFirst) {
|
||||
function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
||||
let timeElapsed = obj.timeElapsed
|
||||
// Start time of a file is never consistent unless 0. Run time of an episode can vary.
|
||||
// When within 30 seconds of start time, just make the time 0 to smooth things out
|
||||
@ -96,7 +98,7 @@ function createLineup(obj, channel, fillers, isFirst) {
|
||||
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
|
||||
special = JSON.parse(JSON.stringify(channel.fallback[0]));
|
||||
}
|
||||
let randomResult = pickRandomWithMaxDuration(channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) );
|
||||
let randomResult = pickRandomWithMaxDuration(programPlayTime, channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) );
|
||||
filler = randomResult.filler;
|
||||
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
|
||||
remaining = randomResult.minimumWait;
|
||||
@ -178,7 +180,7 @@ function weighedPick(a, total) {
|
||||
return random.bool(a, total);
|
||||
}
|
||||
|
||||
function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
|
||||
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration) {
|
||||
let list = [];
|
||||
for (let i = 0; i < fillers.length; i++) {
|
||||
list = list.concat(fillers[i].content);
|
||||
@ -194,16 +196,36 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
|
||||
}
|
||||
let listM = 0;
|
||||
let fillerId = undefined;
|
||||
for (let j = 0; j < fillers.length; j++) {
|
||||
|
||||
for (let medianCheck = 1; medianCheck >= 0; medianCheck--) {
|
||||
for (let j = 0; j < fillers.length; j++) {
|
||||
list = fillers[j].content;
|
||||
let pickedList = false;
|
||||
let n = 0;
|
||||
|
||||
let maximumPlayTimeAllowed = INFINITE_TIME;
|
||||
if (medianCheck==1) {
|
||||
//calculate the median
|
||||
let median = getFillerMedian(programPlayTime, channel, fillers[j]);
|
||||
if (median > 0) {
|
||||
maximumPlayTimeAllowed = median - 1;
|
||||
// allow any clip with a play time that's less than the median.
|
||||
} else {
|
||||
// initially all times are 0, so if the median is 0, all of those
|
||||
// are allowed.
|
||||
maximumPlayTimeAllowed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let clip = list[i];
|
||||
// a few extra milliseconds won't hurt anyone, would it? dun dun dun
|
||||
if (clip.duration <= maxDuration + SLACK ) {
|
||||
let t1 = channelCache.getProgramLastPlayTime( channel.number, clip );
|
||||
let t1 = channelCache.getProgramLastPlayTime(programPlayTime, channel.number, clip );
|
||||
if (t1 > maximumPlayTimeAllowed) {
|
||||
continue;
|
||||
}
|
||||
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
|
||||
|
||||
if (timeSince < channel.fillerRepeatCooldown - SLACK) {
|
||||
@ -247,11 +269,13 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pick1 != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let pick = pick1;
|
||||
let pickTitle = "null";
|
||||
if (pick != null) {
|
||||
pickTitle = pick.title;
|
||||
pick = JSON.parse( JSON.stringify(pick) );
|
||||
pick.fillerId = fillerId;
|
||||
}
|
||||
@ -322,6 +346,26 @@ function getWatermark( ffmpegSettings, channel, type) {
|
||||
}
|
||||
|
||||
|
||||
function getFillerMedian(programPlayTime, channel, filler) {
|
||||
|
||||
let times = [];
|
||||
list = filler.content;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let clip = list[i];
|
||||
let t = channelCache.getProgramLastPlayTime(programPlayTime, channel.number, clip);
|
||||
times.push(t);
|
||||
}
|
||||
|
||||
if (times.length <= 1) {
|
||||
//if there are too few elements, the protection is not helpful.
|
||||
return INFINITE_TIME;
|
||||
}
|
||||
let m = Math.floor(times.length / 2);
|
||||
quickselect(times, m)
|
||||
return times[m];
|
||||
|
||||
}
|
||||
|
||||
function generateChannelContext(channel) {
|
||||
let channelContext = {};
|
||||
for (let i = 0; i < CHANNEL_CONTEXT_KEYS.length; i++) {
|
||||
|
||||
@ -18,6 +18,11 @@ class OfflinePlayer {
|
||||
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/loading-screen.png`;
|
||||
context.channel.offlineSoundtrack = undefined;
|
||||
}
|
||||
if (context.isInterlude === true) {
|
||||
context.channel = JSON.parse( JSON.stringify(context.channel) );
|
||||
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/black.png`;
|
||||
context.channel.offlineSoundtrack = undefined;
|
||||
}
|
||||
this.ffmpeg = new FFMPEG(context.ffmpegSettings, context.channel);
|
||||
this.ffmpeg.setAudioOnly(this.context.audioOnly);
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ class ProgramPlayer {
|
||||
// people might want the codec normalization to stay because of player support
|
||||
context.ffmpegSettings.normalizeResolution = false;
|
||||
}
|
||||
context.ffmpegSettings.noRealTime = program.noRealTime;
|
||||
if ( typeof(program.err) !== 'undefined') {
|
||||
console.log("About to play error stream");
|
||||
this.delegate = new OfflinePlayer(true, context);
|
||||
@ -42,6 +43,11 @@ class ProgramPlayer {
|
||||
/* loading */
|
||||
context.isLoading = true;
|
||||
this.delegate = new OfflinePlayer(false, context);
|
||||
} else if (program.type === 'interlude') {
|
||||
console.log("About to play interlude stream");
|
||||
/* interlude */
|
||||
context.isInterlude = true;
|
||||
this.delegate = new OfflinePlayer(false, context);
|
||||
} else if (program.type === 'offline') {
|
||||
console.log("About to play offline stream");
|
||||
/* offline */
|
||||
|
||||
@ -7,7 +7,9 @@ function equalItems(a, b) {
|
||||
if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) {
|
||||
return false;
|
||||
}
|
||||
return ( a.type === b.type);
|
||||
console.log("no idea how to compare this: " + JSON.stringify(a) );
|
||||
console.log(" with this: " + JSON.stringify(b) );
|
||||
return a.title === b.title;
|
||||
|
||||
}
|
||||
|
||||
@ -17,15 +19,14 @@ function wereThereTooManyAttempts(sessionId, lineupItem) {
|
||||
let t1 = (new Date()).getTime();
|
||||
|
||||
let previous = cache[sessionId];
|
||||
let result = false;
|
||||
|
||||
if (typeof(previous) === 'undefined') {
|
||||
previous = cache[sessionId] = {
|
||||
t0: t1 - constants.TOO_FREQUENT * 5,
|
||||
lineupItem: null,
|
||||
};
|
||||
}
|
||||
|
||||
let result = false;
|
||||
if (t1 - previous.t0 < constants.TOO_FREQUENT) {
|
||||
} else if (t1 - previous.t0 < constants.TOO_FREQUENT) {
|
||||
//certainly too frequent
|
||||
result = equalItems( previous.lineupItem, lineupItem );
|
||||
}
|
||||
@ -49,4 +50,4 @@ function wereThereTooManyAttempts(sessionId, lineupItem) {
|
||||
|
||||
}
|
||||
|
||||
module.exports = wereThereTooManyAttempts;
|
||||
module.exports = wereThereTooManyAttempts;
|
||||
|
||||
110
src/video.js
110
src/video.js
@ -18,7 +18,7 @@ async function shutdown() {
|
||||
stopPlayback = true;
|
||||
}
|
||||
|
||||
function video( channelService, fillerDB, db, programmingService, activeChannelService ) {
|
||||
function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ) {
|
||||
var router = express.Router()
|
||||
|
||||
router.get('/setup', (req, res) => {
|
||||
@ -51,7 +51,10 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
})
|
||||
})
|
||||
// Continuously stream video to client. Leverage ffmpeg concat for piecing together videos
|
||||
let concat = async (req, res, audioOnly) => {
|
||||
let concat = async (req, res, audioOnly, step) => {
|
||||
if ( typeof(step) === 'undefined') {
|
||||
step = 0;
|
||||
}
|
||||
if (stopPlayback) {
|
||||
res.status(503).send("Server is shutting down.")
|
||||
return;
|
||||
@ -78,9 +81,11 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'video/mp2t'
|
||||
})
|
||||
if (step == 0) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'video/mp2t'
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`\r\nStream starting. Channel: ${channel.number} (${channel.name})`)
|
||||
|
||||
@ -107,7 +112,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
return;
|
||||
})
|
||||
|
||||
ffmpeg.on('close', stop)
|
||||
//ffmpeg.on('close', stop)
|
||||
|
||||
res.on('close', () => { // on HTTP close, kill ffmpeg
|
||||
console.log(`\r\nStream ended. Channel: ${channel.number} (${channel.name})`);
|
||||
@ -115,13 +120,13 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
})
|
||||
|
||||
ffmpeg.on('end', () => {
|
||||
console.log("Video queue exhausted. Either you played 100 different clips in a row or there were technical issues that made all of the possible 100 attempts fail.")
|
||||
stop();
|
||||
console.log("Queue exhausted so we are appending the channel stream again to the http output.")
|
||||
concat(req, res, audioOnly, step+1);
|
||||
})
|
||||
|
||||
let channelNum = parseInt(req.query.channel, 10)
|
||||
let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}`);
|
||||
ff.pipe(res );
|
||||
let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}&stepNumber={step}`);
|
||||
ff.pipe(res, { end: false} );
|
||||
};
|
||||
router.get('/video', async(req, res) => {
|
||||
return await concat(req, res, false);
|
||||
@ -167,6 +172,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
isFirst = true;
|
||||
}
|
||||
|
||||
let isBetween = ( (typeof req.query.between !== 'undefined') && (req.query.between=='1') );
|
||||
|
||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||
|
||||
// Check if ffmpeg path is valid
|
||||
@ -180,20 +187,37 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
|
||||
|
||||
// Get video lineup (array of video urls with calculated start times and durations.)
|
||||
let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
|
||||
|
||||
let prog = null;
|
||||
let brandChannel = channel;
|
||||
let redirectChannels = [];
|
||||
let upperBounds = [];
|
||||
|
||||
const GAP_DURATION = constants.GAP_DURATION;
|
||||
if (isLoading) {
|
||||
lineupItem = {
|
||||
type: 'loading',
|
||||
streamDuration: 40,
|
||||
duration: 40,
|
||||
title: "Loading Screen",
|
||||
noRealTime: true,
|
||||
streamDuration: GAP_DURATION,
|
||||
duration: GAP_DURATION,
|
||||
redirectChannels: [channel],
|
||||
start: 0,
|
||||
};
|
||||
} else if (lineupItem != null) {
|
||||
} else if (isBetween) {
|
||||
lineupItem = {
|
||||
type: 'interlude',
|
||||
title: "Interlude Screen",
|
||||
noRealTime: true,
|
||||
streamDuration: GAP_DURATION,
|
||||
duration: GAP_DURATION,
|
||||
redirectChannels: [channel],
|
||||
start: 0,
|
||||
};
|
||||
} else {
|
||||
lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
|
||||
}
|
||||
if (lineupItem != null) {
|
||||
redirectChannels = lineupItem.redirectChannels;
|
||||
upperBounds = lineupItem.upperBounds;
|
||||
brandChannel = redirectChannels[ redirectChannels.length -1];
|
||||
@ -208,7 +232,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
if ( !(prog.program.isOffline) || (prog.program.type != 'redirect') ) {
|
||||
break;
|
||||
}
|
||||
channelCache.recordPlayback( brandChannel.number, t0, {
|
||||
channelCache.recordPlayback(programPlayTimeDB,
|
||||
brandChannel.number, t0, {
|
||||
/*type: 'offline',*/
|
||||
title: 'Error',
|
||||
err: Error("Recursive channel redirect found"),
|
||||
@ -274,11 +299,20 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted."
|
||||
}
|
||||
let fillers = await fillerDB.getFillersFromChannel(brandChannel);
|
||||
let lineup = helperFuncs.createLineup(prog, brandChannel, fillers, isFirst)
|
||||
lineupItem = lineup.shift();
|
||||
try {
|
||||
let lineup = helperFuncs.createLineup(programPlayTimeDB, prog, brandChannel, fillers, isFirst)
|
||||
lineupItem = lineup.shift();
|
||||
} catch (err) {
|
||||
console.log("Error when attempting to pick video: " +err.stack);
|
||||
lineupItem = {
|
||||
isOffline: true,
|
||||
err: err,
|
||||
duration : 60000,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ( !isLoading && (lineupItem != null) ) {
|
||||
if ( !isBetween && !isLoading && (lineupItem != null) ) {
|
||||
let upperBound = 1000000000;
|
||||
let beginningOffset = 0;
|
||||
if (typeof(lineupItem.beginningOffset) !== 'undefined') {
|
||||
@ -298,10 +332,13 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
lineupItem.streamDuration = Math.min(u2, u);
|
||||
upperBound = lineupItem.streamDuration;
|
||||
}
|
||||
channelCache.recordPlayback( redirectChannels[i].number, t0, lineupItem );
|
||||
channelCache.recordPlayback( programPlayTimeDB, redirectChannels[i].number, t0, lineupItem );
|
||||
}
|
||||
}
|
||||
|
||||
let t2 = (new Date()).getTime();
|
||||
console.log( `Decision Latency: (${t2-t0})ms` );
|
||||
|
||||
|
||||
console.log("=========================================================");
|
||||
console.log("! Start playback");
|
||||
@ -317,8 +354,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
}
|
||||
console.log("=========================================================");
|
||||
|
||||
if (! isLoading) {
|
||||
channelCache.recordPlayback(channel.number, t0, lineupItem);
|
||||
if (! isLoading && ! isBetween) {
|
||||
channelCache.recordPlayback(programPlayTimeDB, channel.number, t0, lineupItem);
|
||||
}
|
||||
if (wereThereTooManyAttempts(session, lineupItem)) {
|
||||
console.error("There are too many attempts to play the same item in a short period of time, playing the error stream instead.");
|
||||
@ -363,7 +400,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
try {
|
||||
playerObj = await player.play(res);
|
||||
t1 = (new Date()).getTime();
|
||||
console.log("Latency: (" + (t1- t0) );
|
||||
console.log( `Player Latency: (${t1-t0})ms` );
|
||||
} catch (err) {
|
||||
console.log("Error when attempting to play video: " +err.stack);
|
||||
try {
|
||||
@ -523,6 +560,10 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
return
|
||||
}
|
||||
|
||||
let stepNumber = parseInt(req.query.stepNumber, 10)
|
||||
if (isNaN(stepNumber)) {
|
||||
stepNumber = 0;
|
||||
}
|
||||
let channelNum = parseInt(req.query.channel, 10)
|
||||
let channel = await channelService.getChannel(channelNum );
|
||||
if (channel == null) {
|
||||
@ -541,20 +582,35 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
||||
let sessionId = StreamCount++;
|
||||
let audioOnly = ("true" == req.query.audioOnly);
|
||||
|
||||
if (
|
||||
(ffmpegSettings.enableFFMPEGTranscoding === true)
|
||||
let transcodingEnabled = (ffmpegSettings.enableFFMPEGTranscoding === true)
|
||||
&& (ffmpegSettings.normalizeVideoCodec === true)
|
||||
&& (ffmpegSettings.normalizeAudioCodec === true)
|
||||
&& (ffmpegSettings.normalizeResolution === true)
|
||||
&& (ffmpegSettings.normalizeAudio === true)
|
||||
&& (ffmpegSettings.normalizeAudio === true);
|
||||
|
||||
if (
|
||||
transcodingEnabled
|
||||
&& (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) */
|
||||
&& (stepNumber == 0)
|
||||
) {
|
||||
//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}&audioOnly=${audioOnly}'\n`
|
||||
for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) {
|
||||
let remaining = maxStreamsToPlayInARow;
|
||||
if (stepNumber == 0) {
|
||||
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
|
||||
|
||||
if (transcodingEnabled && (audioOnly !== true)) {
|
||||
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&between=1&session=${sessionId}&audioOnly=${audioOnly}'\n`;
|
||||
}
|
||||
remaining--;
|
||||
}
|
||||
|
||||
for (var i = 0; i < remaining; i++) {
|
||||
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}&audioOnly=${audioOnly}'\n`
|
||||
if (transcodingEnabled && (audioOnly !== true) ) {
|
||||
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&between=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
|
||||
}
|
||||
}
|
||||
|
||||
res.send(data)
|
||||
|
||||
@ -8,6 +8,10 @@ module.exports = function ($scope, $timeout, dizquetv) {
|
||||
$scope.shows = [ { id: '?', pending: true} ]
|
||||
$timeout();
|
||||
let shows = await dizquetv.getAllShowsInfo();
|
||||
shows.sort( (a,b) => {
|
||||
return a.name > b.name;
|
||||
} );
|
||||
|
||||
$scope.shows = shows;
|
||||
$timeout();
|
||||
}
|
||||
|
||||
@ -8,13 +8,14 @@ module.exports = function ($scope, $timeout, dizquetv) {
|
||||
$scope.fillers = [ { id: '?', pending: true} ]
|
||||
$timeout();
|
||||
let fillers = await dizquetv.getAllFillersInfo();
|
||||
fillers.sort( (a,b) => {
|
||||
return a.name > b.name;
|
||||
} );
|
||||
$scope.fillers = fillers;
|
||||
$timeout();
|
||||
}
|
||||
$scope.refreshFiller();
|
||||
|
||||
|
||||
|
||||
let feedToFillerConfig = () => {};
|
||||
let feedToDeleteFiller = feedToFillerConfig;
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
||||
},
|
||||
link: {
|
||||
|
||||
post: function (scope, element, attrs) {
|
||||
post: function (scope, $element, attrs) {
|
||||
scope.screenW = 1920;
|
||||
scope.screenh = 1080;
|
||||
|
||||
@ -326,9 +326,10 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
||||
duration: duration,
|
||||
isOffline: true
|
||||
}
|
||||
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
|
||||
scope.channel.programs.splice(scope.channel.programs.length, 0, program);
|
||||
scope._selectedOffline = null
|
||||
scope._addingOffline = null;
|
||||
scrollToLast();
|
||||
updateChannelDuration()
|
||||
}
|
||||
|
||||
@ -1077,11 +1078,37 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
||||
}
|
||||
}
|
||||
|
||||
scope.importPrograms = (selectedPrograms) => {
|
||||
|
||||
function getAllMethods(object) {
|
||||
|
||||
return Object.getOwnPropertyNames(object).filter(function (p) {
|
||||
return typeof object[p] == 'function';
|
||||
});
|
||||
}
|
||||
function scrollToLast() {
|
||||
var programListElement = document.getElementById("channelConfigProgramList");
|
||||
$timeout(() => { programListElement.scrollTo(0, 2000000); }, 0)
|
||||
}
|
||||
scope.importPrograms = (selectedPrograms, insertPoint) => {
|
||||
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
|
||||
delete selectedPrograms[i].commercials;
|
||||
}
|
||||
scope.channel.programs = scope.channel.programs.concat(selectedPrograms)
|
||||
|
||||
var programListElement = document.getElementById("channelConfigProgramList");
|
||||
if (insertPoint === "start") {
|
||||
scope.channel.programs = selectedPrograms.concat(scope.channel.programs);
|
||||
programListElement.scrollTo(0, 0);
|
||||
} else if (insertPoint === "current") {
|
||||
scope.channel.programs = [
|
||||
...scope.channel.programs.slice(0, scope.currentStartIndex),
|
||||
...selectedPrograms,
|
||||
...scope.channel.programs.slice(scope.currentStartIndex)
|
||||
];
|
||||
} else {
|
||||
scope.channel.programs = scope.channel.programs.concat(selectedPrograms)
|
||||
|
||||
scrollToLast();
|
||||
}
|
||||
updateChannelDuration()
|
||||
setTimeout(
|
||||
() => {
|
||||
@ -1093,7 +1120,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
||||
}
|
||||
scope.finishRedirect = (program) => {
|
||||
if (scope.selectedProgram == -1) {
|
||||
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
|
||||
scope.channel.programs.splice(scope.channel.programs.length, 0, program);
|
||||
scrollToLast();
|
||||
|
||||
} else {
|
||||
scope.channel.programs[ scope.selectedProgram ] = program;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
scope: {
|
||||
onFinish: "=onFinish",
|
||||
height: "=height",
|
||||
positionChoice: "=positionChoice",
|
||||
visible: "=visible",
|
||||
limit: "=limit",
|
||||
},
|
||||
@ -14,6 +15,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
if ( typeof(scope.limit) == 'undefined') {
|
||||
scope.limit = 1000000000;
|
||||
}
|
||||
scope.insertPoint = "end";
|
||||
scope.customShows = [];
|
||||
scope.origins = [];
|
||||
scope.currentOrigin = undefined;
|
||||
@ -37,7 +39,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
updateCustomShows();
|
||||
}
|
||||
}
|
||||
scope._onFinish = (s) => {
|
||||
scope._onFinish = (s, insertPoint) => {
|
||||
if (s.length > scope.limit) {
|
||||
if (scope.limit == 1) {
|
||||
scope.error = "Please select only one clip.";
|
||||
@ -45,7 +47,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
scope.error = `Please select at most ${scope.limit} clips.`;
|
||||
}
|
||||
} else {
|
||||
scope.onFinish(s)
|
||||
scope.onFinish(s, insertPoint)
|
||||
scope.selection = []
|
||||
scope.visible = false
|
||||
}
|
||||
@ -69,7 +71,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
||||
}
|
||||
}
|
||||
scope.selectLibrary = async (library) => {
|
||||
await scope.fillNestedIfNecessary(library);
|
||||
await scope.fillNestedIfNecessary(library, true);
|
||||
let p = library.nested.length;
|
||||
scope.pending += library.nested.length;
|
||||
try {
|
||||
|
||||
@ -390,4 +390,8 @@ div.programming-programs div.list-group-item {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.list-group-item .library-item-hover:hover {
|
||||
background: #D0D0FF
|
||||
}
|
||||
@ -170,6 +170,7 @@
|
||||
ng-init="setUpWatcher()"
|
||||
ng-if="true"
|
||||
ng-style="{'max-height':programmingHeight()}"
|
||||
id="channelConfigProgramList"
|
||||
>
|
||||
<div ng-repeat="x in channel.programs track by x.$index"
|
||||
ng-click="selectProgram(x.$index)"
|
||||
@ -938,7 +939,7 @@
|
||||
<frequency-tweak programs="_programFrequencies" message="_frequencyMessage" modified="_frequencyModified" on-done="tweakFrequencies"></frequency-tweak>
|
||||
<remove-shows program-infos="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
|
||||
<flex-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></flex-config>
|
||||
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" position-choice=true on-finish="importPrograms"></plex-library>
|
||||
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
|
||||
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
|
||||
<time-slots-schedule-editor linker="registerTimeSlots" on-done="onTimeSlotsDone"></time-slots-schedule-editor>
|
||||
|
||||
@ -28,46 +28,59 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<select class="form-control form-control-sm custom-select" ng-model="currentOrigin"
|
||||
<div class="mb-3">
|
||||
<label for="source-selector" class="form-label">Source:</label>
|
||||
<select class="form-select form-select-sm custom-select" ng-model="currentOrigin"
|
||||
size="2"
|
||||
id="source-selector"
|
||||
ng-options="x.name for x in origins" ng-change="selectOrigin(currentOrigin)"></select>
|
||||
<hr ></hr>
|
||||
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<button class="btn btn-sm btn-link" ng-click="selectOrigin(currentOrigin)">
|
||||
<span class="text-info fa fa-sync" ></span>
|
||||
</button>
|
||||
Content:
|
||||
</label>
|
||||
<ul ng-show="currentOrigin.type=='plex' " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
|
||||
<li class="list-group-item" ng-repeat="a in libraries">
|
||||
<div class="flex-container {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
|
||||
<div class="flex-container library-item-hover {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
|
||||
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
||||
<img ng-if="displayImages" lazy-img="{{a.icon}}" ></img>
|
||||
<span>{{ displayTitle(a) }}</span><!-- Library -->
|
||||
<span ng-if="a.type === 'show' || a.type === 'movie' || a.type === 'artist'" class="flex-pull-right" ng-click='$event.stopPropagation(); selectLibrary(a)'>
|
||||
<span class="fa fa-plus btn"></span>
|
||||
<span class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
</div>
|
||||
<ul ng-if="a.collapse" class="list-group">
|
||||
<li class="list-group-item {{ b.type !== 'movie' ? 'list-group-item-secondary' : 'list-group-item-video' }}"
|
||||
ng-repeat="b in a.nested">
|
||||
<div class="flex-container"
|
||||
<div class="flex-container library-item-hover"
|
||||
ng-click="b.type !== 'movie' ? getNested(b) : selectItem(b, true)">
|
||||
<span ng-if="b.type === 'movie'" class="fa fa-plus-circle tab"></span>
|
||||
<span ng-if="b.type !== 'movie'" class="tab"></span>
|
||||
<span ng-if="b.type !== 'movie'" class="fa {{ b.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
||||
<img ng-if="displayImages" lazy-img="{{ b.type === 'episode' ? b.episodeIcon : b.icon }}" ></img>
|
||||
{{ displayTitle(b) }}
|
||||
<span ng-if="b.type === 'movie'" class="flex-pull-right">
|
||||
<span class="flex-grow-1">{{ displayTitle(b) }}</span>
|
||||
<span ng-if="b.type === 'movie'" class="">
|
||||
{{b.durationStr}}
|
||||
</span>
|
||||
<span class="flex-pull-right" ng-if="b.type === 'movie'">
|
||||
<span class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
<span ng-if="b.type === 'playlist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectPlaylist(b);">
|
||||
<span class="fa fa-plus btn"></span>
|
||||
<span class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
<span ng-if="b.type === 'show' || b.type === 'collection' || b.type === 'genre' || b.type === 'artist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectShow(b);">
|
||||
<span class="fa fa-plus btn"></span>
|
||||
<span class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
</div>
|
||||
<ul ng-if="b.collapse" class="list-group">
|
||||
<li ng-repeat="c in b.nested"
|
||||
class="list-group-item {{ c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? 'list-group-item-dark' : 'list-group-item-video' }}">
|
||||
<div class="flex-container"
|
||||
<div class="flex-container library-item-hover"
|
||||
ng-click="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? getNested(c) : selectItem(c, true)">
|
||||
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track'"
|
||||
class="fa fa-plus-circle tab"></span>
|
||||
|
||||
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
||||
class="tab"></span>
|
||||
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
||||
@ -75,23 +88,30 @@
|
||||
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
||||
class="fa {{ c.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
||||
<img ng-if="displayImages" lazy-img="{{c.type === 'episode' ? c.episodeIcon : c.icon }}" ></img>
|
||||
{{ displayTitle(c) }}
|
||||
<span class="flex-grow-1">{{ displayTitle(c) }}</span>
|
||||
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track' "
|
||||
class="flex-pull-right">
|
||||
class="">
|
||||
{{c.durationStr}}
|
||||
</span>
|
||||
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track'" class="flex-pull-right">
|
||||
<span
|
||||
class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
<span ng-if="c.type === 'season' || c.type === 'album'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectSeason(c);">
|
||||
<span class="fa fa-plus btn"></span>
|
||||
<span class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
</div>
|
||||
<ul ng-if="c.collapse" class="list-group">
|
||||
<li class="list-group-item list-group-item-video"
|
||||
ng-repeat="d in c.nested">
|
||||
<div class="flex-container" ng-click="selectItem(d, true)">
|
||||
<span class="fa fa-plus-circle tab"></span>
|
||||
<div class="flex-container library-item-hover" ng-click="selectItem(d, true)">
|
||||
|
||||
<img ng-if="displayImages" lazy-img="{{d.episodeIcon}}" ></img>
|
||||
{{ displayTitle(d) }}
|
||||
<span class="flex-pull-right">{{d.durationStr}}</span>
|
||||
<span class="flex-grow-1">{{ displayTitle(d) }}</span>
|
||||
<span class="">{{d.durationStr}}</span>
|
||||
<span class="flex-pull-right">
|
||||
<span class="fa fa-plus-circle btn"></span>
|
||||
</span>
|
||||
<!-- Episode -->
|
||||
</div>
|
||||
</li>
|
||||
@ -111,6 +131,7 @@
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr></hr>
|
||||
<div class="loader" ng-if="pending > 0" ></div> <h6 style='display:inline-block'>Selected Items</h6>
|
||||
@ -129,9 +150,21 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class='text-danger'>{{error}}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-link" ng-click="_onFinish([])">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="_onFinish(selection);">Done</button>
|
||||
|
||||
<div class="modal-footer flex">
|
||||
|
||||
<div class="flex-grow-1" ng-show="positionChoice === true"">
|
||||
<select class="form-select form-select-sm custom-select" ng-model="insertPoint"
|
||||
id="position-selector">
|
||||
<option value="end">Insert at the end of list</option>
|
||||
<option value="start">Insert at the beginning of list</option>
|
||||
<option value="current">Insert at current scroll position</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<div><button type="button" class="btn btn-sm btn-link" ng-click="_onFinish([])">Cancel</button></div>
|
||||
<div><button type="button" class="btn btn-sm btn-primary" ng-click="_onFinish(selection, insertPoint);">Done</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -283,7 +283,7 @@ module.exports = function ($http, $window, $interval) {
|
||||
console.error(msg , err);
|
||||
}
|
||||
}
|
||||
if ( (includeCollections === true) && (res.viewGroup !== "artist" ) ) {
|
||||
if (includeCollections === true) {
|
||||
let k = res.librarySectionID;
|
||||
|
||||
k = `/library/sections/${k}/collections`;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user