Compare commits

...
99 Commits
Author SHA1 Message Date
Jason Dove fd3ef90880 update changelog for release v0.4.2-alpha [no ci] 2022-02-26 19:23:18 -06:00
Jason Dove 696b29c9e9 fix videotoolbox acceleration with new transcoder (#658)
* fix videotoolbox acceleration with new transcoder

* cleanup
2022-02-26 18:45:05 -06:00
Jason Dove 70c37df596 fix vaapi watermarks with new transcoder (#657) 2022-02-26 14:00:23 -06:00
Jason Dove 040785b0d7 fix qsv scaling and watermarks with new transcoder (#656)
* fix qsv scaling

* fix qsv watermarks
2022-02-26 11:40:16 -06:00
Jason Dove b25f783343 hide unused local libraries (#655) 2022-02-25 21:16:38 -06:00
Jason Dove a21f62ff8c add watermark support to experimental transcoder logic (#654)
* wip

* fix songs again

* don't use lists of video and audio input files

* test concat command output

* move concat pipeline

* start to use ffmpeg state

* add ffmpeg state and audio state

* audio state is required

* attach input options directly to input files

* move filters to input files

* add watermark support
2022-02-25 21:06:17 -06:00
Jason Dove 78fdc9c57a add option to shuffle schedule items (#652)
* add schedule setting

* it works

* fix tests

* update readme

* rebuild all playouts
2022-02-22 21:20:40 -06:00
Jason Dove f6c42f3ff5 add configurable channel group and categories (#651) 2022-02-21 21:04:12 -06:00
Jason Dove c92b6cb909 fix song playback (#644) 2022-02-17 21:52:28 -06:00
Jason Dove a2e1dc8bfb don't deinterlace using nvidia decoders (#643) 2022-02-17 13:28:03 -06:00
Jason Dove 8a6093ce8d properly specify audio codec even when source has correct format (#641) 2022-02-16 11:29:54 -06:00
Jason Dove 1d6279cee8 log problematic playlists (#640) 2022-02-16 08:18:22 -06:00
Jason Dove 66ab0b3990 use single thread and disable framerate normalization (#639)
* try one thread for everything

* add (unused) framerate filter

* disable framerate normalization by default

* update dependencies
2022-02-16 06:01:28 -06:00
Jason Dove a7922beaed qsv and logging fixes (#637)
* improve pipeline logging

* fix qsv acceleration

* fix qsv parameter order
2022-02-15 12:41:06 -06:00
Jason Dove a1d9d6790e fix copy codec when transcoding is disabled (#636) 2022-02-15 09:22:40 -06:00
Jason Dove 2f2d7952dd use vaapi driver settings with new transcoder logic (#635)
* add LIBVA_DRIVER_NAME env var

* add vaapi device name

* add FFREPORT env var

* fixes
2022-02-15 08:44:49 -06:00
Jason Dove c96b800b52 ffmpeg lib fixes (#633)
* try to fix vaapi inconsistencies

* log unexpected data

* vaapi fixes

* disable 444 test

* add qsv deinterlace filter; qsv fixes

* add videotoolbox acceleration
2022-02-14 22:01:26 -06:00
Jason Dove c05882f4a6 fix docker builds 2022-02-14 14:36:35 -06:00
Jason Dove 5a442a06a0 start to use new ffmpeg library (#632)
* start to add ffmpeg library

* start to hook ffmpeg lib into main app

* improvements

* more progress

* make pipeline builder configurable

* more options

* move more logic down into ffmpeg lib

* ffmpeg lib desired state refactoring

* add software scaling and padding

* add loudness normalization and software deinterlace

* add metadata output options

* add setsar filter

* use built-in scaling logic

* fixes

* initial nvidia support

* nvidia improvements

* support hls mode

* print old arguments at debug level

* fix package reference

* start to add qsv support

* formatting

* fix tests

* add timeout to transcode tests

* show successful ffmpeg arguments

* add vaapi support

* add more software decoders

* add experimental transcoder option

* call existing ffmpeg process service for unimplemented features

* fix nvidia mpeg2video bug

* update changelog

* ignore some neglected unit tests
2022-02-14 14:34:00 -06:00
Jason Dove 640fed0a43 fix hls segmenter bug with unknown packet duration 2022-02-11 18:37:02 -06:00
Jason Dove ab1f294c1f update changelog for release v0.4.1-alpha 2022-02-10 20:49:26 -06:00
Jason Dove ea08453913 vaapi improvements (#629)
* fix interlaced video with vaapi

* downgrade imagesharp to fix blurhash generation

* fix ui crash loading collection editor
2022-02-10 20:19:59 -06:00
Jason Dove 87deaa6f3a nvidia improvements (#628) 2022-02-10 15:39:45 -06:00
Jason Dove 9d99c19ea4 fix playback with unknown pixel format (#627) 2022-02-10 08:37:25 -06:00
Jason Dove 49d14b05f6 update more dependencies (#626) 2022-02-09 11:08:49 -06:00
Jason Dove a8ba9edf2b update dependencies (#624)
* update dependencies

* include refit xml serializer
2022-02-09 10:10:12 -06:00
Jason Dove 89811a1203 wait for one segment by default (#617) 2022-02-07 12:44:12 -06:00
Jason Dove 534e2c4512 add hls segmenter initial segment count (#616) 2022-02-07 12:18:58 -06:00
dependabot[bot] c1e148633d Bump Blazored.LocalStorage from 4.1.5 to 4.2.0 (#614)
Bumps [Blazored.LocalStorage](https://github.com/Blazored/LocalStorage) from 4.1.5 to 4.2.0.
- [Release notes](https://github.com/Blazored/LocalStorage/releases)
- [Commits](https://github.com/Blazored/LocalStorage/compare/v4.1.5...v4.2.0)

---
updated-dependencies:
- dependency-name: Blazored.LocalStorage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-07 03:10:34 -06:00
Jason Dove a9dff5eff7 properly flag local missing folders (#615) 2022-02-07 02:39:24 -06:00
Jason Dove a2da043f4b try to fix mac permission issues 2022-02-06 17:45:28 -06:00
dependabot[bot] 252c185562 Bump MudBlazor from 6.0.5 to 6.0.6 (#609)
Bumps [MudBlazor](https://github.com/MudBlazor/MudBlazor) from 6.0.5 to 6.0.6.
- [Release notes](https://github.com/MudBlazor/MudBlazor/releases)
- [Changelog](https://github.com/MudBlazor/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/MudBlazor/MudBlazor/compare/v6.0.5...v6.0.6)

---
updated-dependencies:
- dependency-name: MudBlazor
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-05 18:20:45 -06:00
Jason Dove a47987a9d7 revert windows trimming (#613)
* Revert "disable trimming in docker"

This reverts commit 5937211bb8.

* Revert "try to reduce windows artifact size"

This reverts commit e32dbd0474.
2022-02-05 18:09:02 -06:00
Jason Dove 5937211bb8 disable trimming in docker 2022-02-05 13:41:47 -06:00
Jason Dove e32dbd0474 try to reduce windows artifact size 2022-02-05 13:34:21 -06:00
Jason Dove 6bcc1ede2b try again 2022-02-05 11:25:14 -06:00
Jason Dove 6c9764a51e try a different method for downloading ffmpeg 2022-02-05 11:19:56 -06:00
Jason Dove ff5438459c try to bundle ffmpeg with windows artifacts 2022-02-05 11:09:01 -06:00
Jason Dove 0c53a4509c show collection name in some error messages (#612) 2022-02-04 20:17:17 -06:00
Jason Dove 5fd315ead8 change framerate normalization method (#611) 2022-02-04 14:39:27 -06:00
Jason Dove f02b0ac345 re-introduce framerate normalization (#610) 2022-02-04 12:57:40 -06:00
Jason Dove fd83007296 try to fix watermark on vaapi 2022-02-01 17:50:47 -06:00
Jason Dove 70ca5bf050 fix bug with watermark and short content (#608) 2022-01-31 14:07:12 -06:00
Jason Dove eed9f60273 fade in and fade out intermittent watermarks (#607)
* first pass at fading in/out overlay

* fix tests

* update changelog
2022-01-31 12:31:56 -06:00
Jason Dove 0e2e6cd52e build linux-arm64 artifacts 2022-01-31 01:07:03 -06:00
Jason Dove c9b557f2e6 more xmltv category improvements (#606) 2022-01-30 17:58:45 -06:00
Jason Dove cde869f3eb enable docker dependency scanning [no build] 2022-01-30 11:50:16 -06:00
Jason Dove 90d6a59d3f normalize smart quotes in search queries (#605) 2022-01-30 11:09:32 -06:00
Jason Dove b972947747 xmltv category improvements (#604) 2022-01-30 10:56:53 -06:00
Jason Dove 17bc988b49 update changelog for release v0.4.0-alpha [no ci] 2022-01-29 18:22:07 -06:00
Jason Dove 749eea836b update install docs for tray apps (win, mac) [no build] 2022-01-29 18:18:58 -06:00
Jason Dove 37c52c4cb4 update docs and dependencies (#603) 2022-01-29 18:08:05 -06:00
Jason Dove 33ba58aa68 add windows launcher (#602) 2022-01-29 17:58:11 -06:00
Jason Dove 5f6043e593 index added date (#601) 2022-01-29 14:47:58 -06:00
Jason Dove 96e95a21fb update changelog [no ci] 2022-01-29 12:34:10 -06:00
Jason Dove 9168fd6bf2 write text file logs (#600) 2022-01-29 12:31:22 -06:00
Jason Dove 14413f62a7 properly sent content root on macos 2022-01-29 11:49:38 -06:00
Jason Dove 34c71a0c12 try to fix static resource loading 2022-01-29 10:20:03 -06:00
Jason Dove a487e7fe15 use absolute paths in bundle script 2022-01-28 22:11:50 -06:00
Jason Dove cd4ea42597 fix bundle script 2022-01-28 22:05:20 -06:00
Jason Dove a3d42145f7 update macos submodule 2022-01-28 21:57:16 -06:00
Jason Dove 261cf5052a fetch submodules for mac build 2022-01-28 21:48:29 -06:00
Jason Dove de9af2f0f6 first pass at native macos app 2022-01-28 21:41:26 -06:00
Jason Dove 8d4e18ed2f update mac app icon (#599) 2022-01-28 19:46:34 -06:00
Jason Dove 1ee01c1d78 fix hls timestamps (#598) 2022-01-27 23:51:18 -06:00
Jason Dove 7de50dd916 minor hls segmenter improvements (#593) 2022-01-26 20:12:29 -06:00
Jason Dove 744fd3beaa link file not found health check to trash (#592)
* update dependencies

* fix file not found health check
2022-01-26 08:37:40 -06:00
Jason Dove 861c95e1bd fix m3u mode override (#590) 2022-01-25 18:36:54 -06:00
Jason Dove bb5b9f9be4 update changelog for release v0.3.8-alpha [no ci] 2022-01-23 21:46:00 -06:00
Jason Dove 135628441a re-add mac launcher script 2022-01-23 21:09:04 -06:00
Jason Dove 4aa7204984 fix ts mode with hdhr clients (#588) 2022-01-23 18:58:15 -06:00
Jason Dove 1af59a0337 don't use macos launcher script 2022-01-23 18:47:00 -06:00
Jason Dove c4c97fcc8c customize mac dmg 2022-01-23 18:36:31 -06:00
Jason Dove 9c46e42792 fix gon variables 2022-01-23 14:21:13 -06:00
Jason Dove efa803aab6 split mac artifacts job 2022-01-23 14:10:57 -06:00
Jason Dove 6ea02a2d77 use proper version number in ci artifacts [no docs] [no build] 2022-01-23 14:04:13 -06:00
Jason Dove 631f7d2d5e don't use reserved secret name 2022-01-23 13:54:37 -06:00
Jason Dove e44a4cb2e1 properly pass secrets between workflows 2022-01-23 13:53:31 -06:00
Jason Dove f4b95419a6 properly pass data between jobs 2022-01-23 13:46:47 -06:00
Jason Dove 1a5cf49563 refactor reusable docker workflow (#587)
* refactor reusable docker workflow

* refactor reusable artifacts workflow

* fix name

* try to fix

* fix
2022-01-23 13:42:01 -06:00
Jason Dove efef0b0fee don't use single file for mac bundles 2022-01-22 17:29:35 -06:00
Jason Dove ee7b8a71ab fix docker builds [no build] 2022-01-22 14:19:49 -06:00
Jason Dove e7c9a51e96 macos app bundle (#585)
* test signed app bundle

* fix vars

* fix condition

* typo

* fix quoting

* use recursive signing script

* fix release cleanup

* restore proper ci action
2022-01-22 14:03:42 -06:00
Jason Dove 78a954f365 link to development builds in install docs 2022-01-21 20:55:38 -06:00
Jason Dove 355c0b7be9 try to fix deleting old assets 2022-01-21 20:29:50 -06:00
Jason Dove 3bcb2d36f9 another attempt at publishing artifacts 2022-01-21 20:21:26 -06:00
Jason Dove b240de9d4a publish develop artifacts to stable release url 2022-01-21 18:44:25 -06:00
Jason Dove f5001837cb properly separate build artifacts 2022-01-21 18:20:51 -06:00
Jason Dove 6ea916b1f0 fix fetch depth 2022-01-21 15:30:40 -06:00
Jason Dove db6fd22215 try to fix build 2022-01-21 15:25:03 -06:00
Jason Dove 691842008d upload develop binaries for every merge to main (#584)
* upload develop binaries for every merge to main

* rename step
2022-01-21 15:18:08 -06:00
Jason Dove 685f78bef8 fix search results bug (#583) 2022-01-21 14:05:09 -06:00
Jason Dove 3ce267863b fix hls segmenter in some cultures (#582) 2022-01-21 10:42:33 -06:00
Jason Dove e4231cb57d upgrade from ffmpeg 4.4 to 5.0 (#581) 2022-01-20 20:57:38 -06:00
Jason Dove 03946b13ca always use a single ffmpeg thread with realtime (#580) 2022-01-20 14:53:13 -06:00
Jason Dove f1a81bf086 clarify library kind/media kind support (#579) [no docker] 2022-01-19 09:11:23 -06:00
Jason Dove 7a88374362 clarify flood scheduling calc [no ci] 2022-01-18 18:22:46 -06:00
Jason Dove 663a62431b properly fix startup paths (#576) 2022-01-17 16:31:22 -06:00
Jason Dove 1d4acc284d Update changelog for release v0.3.7-alpha [no ci] 2022-01-17 15:23:39 -06:00
343 changed files with 35417 additions and 1267 deletions
+18
View File
@@ -6,3 +6,21 @@ updates:
interval: daily interval: daily
assignees: assignees:
- jasongdove - jasongdove
- package-ecosystem: docker
directory: "/docker"
schedule:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker/nvidia"
schedule:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker/vaapi"
schedule:
interval: daily
assignees:
- jasongdove
+235
View File
@@ -0,0 +1,235 @@
name: Build Artifacts
on:
workflow_call:
inputs:
release_tag:
description: 'Release tag'
required: true
type: string
release_version:
description: 'Release version number (e.g. v0.3.7-alpha)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
secrets:
apple_developer_certificate_p12_base64:
required: true
apple_developer_certificate_password:
required: true
ac_username:
required: true
ac_password:
required: true
gh_token:
required: true
jobs:
build_and_upload_mac:
name: Mac Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: macos-latest
kind: macOS
target: osx-x64
- os: macos-latest
kind: macOS
target: osx-arm64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
p12-password: ${{ secrets.apple_developer_certificate_password }}
- name: Calculate Release Name
shell: bash
run: |
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
- name: Build
shell: bash
run: dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
- name: Bundle
shell: bash
run: |
brew install coreutils
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
scripts/macOS/bundle.sh
- name: Sign
shell: bash
run: scripts/macOS/sign.sh
- name: Create DMG
shell: bash
run: |
brew install create-dmg
create-dmg \
--volname "ErsatzTV" \
--volicon "artwork/ErsatzTV.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "ErsatzTV.app" 200 190 \
--hide-extension "ErsatzTV.app" \
--app-drop-link 600 185 \
"ErsatzTV.dmg" \
"ErsatzTV.app/"
- name: Notarize
shell: bash
run: |
brew tap mitchellh/gon
brew install mitchellh/gon/gon
gon -log-level=debug -log-json ./gon.json
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Cleanup
shell: bash
run: |
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
rm -r publish
rm -r ErsatzTV.app
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.dmg
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.dmg
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_and_upload:
name: Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: ubuntu-latest
kind: linux
target: linux-arm64
- os: windows-latest
kind: windows
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- uses: suisei-cn/actions-download-file@v1
if: ${{ matrix.kind }} == "windows"
id: downloadffmpeg
name: Download ffmpeg
with:
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build.7z"
target: ffmpeg/
- name: Build
shell: bash
run: |
# Define some variables for things we need
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
# Build Windows launcher
if [ "${{ matrix.kind }}" == "windows" ]; then
dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
fi
# Download ffmpeg
if [ "${{ matrix.kind }}" == "windows" ]; then
7z e "ffmpeg/${{ steps.downloadffmpeg.outputs.filename }}" -o"$release_name" '*.exe' -r
rm -f "$release_name/ffplay.exe"
fi
# Pack files
if [ "${{ matrix.kind }}" == "windows" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.zip
${{ env.RELEASE_NAME }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
+39 -91
View File
@@ -1,49 +1,19 @@
name: Build name: Build
on: on:
workflow_dispatch: workflow_dispatch:
pull_request:
push: push:
branches: branches:
- main - main
jobs: jobs:
build_and_test: calculate_version:
runs-on: ${{ matrix.os }} name: Calculate version information
strategy: runs-on: ubuntu-latest
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
steps: steps:
- name: Get the sources - name: Get the sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
build_and_push:
name: Build & Publish to Docker Hub
needs: build_and_test
runs-on: ubuntu-latest
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
steps:
- name: Checkout
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Extract Docker Tag
- name: Extract Git Tag
shell: bash shell: bash
run: | run: |
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
@@ -51,60 +21,38 @@ jobs:
short=$(git rev-parse --short HEAD) short=$(git rev-parse --short HEAD)
final="${tag2/alpha/$short}" final="${tag2/alpha/$short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV echo "GIT_TAG=${final}" >> $GITHUB_ENV
- name: Extract Artifacts Version
- name: Set up Docker Buildx Base shell: bash
uses: docker/setup-buildx-action@v1 run: |
id: builder-base tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
- name: Set up Docker Buildx NVIDIA final="${tag/alpha/$short}"
uses: docker/setup-buildx-action@v1 echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
id: builder-nvidia echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
- name: Set up Docker Buildx VAAPI git_tag: ${{ env.GIT_TAG }}
uses: docker/setup-buildx-action@v1 artifacts_version: ${{ env.ARTIFACTS_VERSION }}
id: builder-vaapi info_version: ${{ env.INFO_VERSION }}
build_and_upload:
- name: Login to DockerHub uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
uses: docker/login-action@v1 needs: calculate_version
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} release_tag: develop
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
- name: Build and push base secrets:
uses: docker/build-push-action@v2 apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
with: apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
builder: ${{ steps.builder-base.outputs.name }} ac_username: ${{ secrets.AC_USERNAME }}
context: . ac_password: ${{ secrets.AC_PASSWORD }}
file: ./docker/Dockerfile gh_token: ${{ secrets.GITHUB_TOKEN }}
push: true build_and_push:
build-args: | uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
INFO_VERSION=${{ env.GIT_TAG }}-docker needs: calculate_version
tags: | with:
jasongdove/ersatztv:develop base_version: develop
jasongdove/ersatztv:${{ github.sha }} info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ github.sha }}
- name: Build and push nvidia secrets:
uses: docker/build-push-action@v2 docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
with: docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:develop-nvidia
jasongdove/ersatztv:${{ github.sha }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:develop-vaapi
jasongdove/ersatztv:${{ github.sha }}-vaapi
+88
View File
@@ -0,0 +1,88 @@
name: Build & Publish to Docker Hub
on:
workflow_call:
inputs:
base_version:
description: 'Base version (latest or develop)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
tag_version:
description: 'Docker tag version (e.g. v0.3.7)'
required: true
type: string
secrets:
docker_hub_username:
required: true
docker_hub_access_token:
required: true
jobs:
build_and_push:
name: Build & Publish
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, '[no build]') == false
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx Base
uses: docker/setup-buildx-action@v1
id: builder-base
- name: Set up Docker Buildx NVIDIA
uses: docker/setup-buildx-action@v1
id: builder-nvidia
- name: Set up Docker Buildx VAAPI
uses: docker/setup-buildx-action@v1
id: builder-vaapi
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.docker_hub_username }}
password: ${{ secrets.docker_hub_access_token }}
- name: Build and push base
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-base.outputs.name }}
context: .
file: ./docker/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}
jasongdove/ersatztv:${{ inputs.tag_version }}
- name: Build and push nvidia
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker-nvidia
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}-nvidia
jasongdove/ersatztv:${{ inputs.tag_version }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker-vaapi
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}-vaapi
jasongdove/ersatztv:${{ inputs.tag_version }}-vaapi
-1
View File
@@ -3,7 +3,6 @@ on:
push: push:
branches: branches:
- main - main
jobs: jobs:
build: build:
name: Deploy docs name: Deploy docs
+30
View File
@@ -0,0 +1,30 @@
name: Pull Request
on:
pull_request:
jobs:
build_and_test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
steps:
- name: Get the sources
uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
+39 -128
View File
@@ -1,142 +1,53 @@
name: Publish name: Release
on: on:
release: release:
types: [ published ] types: [ published ]
jobs: jobs:
release: calculate_version:
name: Release name: Calculate version information
strategy: runs-on: ubuntu-latest
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: windows-latest
kind: windows
target: win-x64
- os: macos-latest
kind: macOS
target: osx-x64
- os: macos-latest
kind: macOS
target: osx-arm64
runs-on: ${{ matrix.os }}
steps: steps:
- name: Get the sources - name: Get the sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
shell: bash
run: |
# Define some variables for things we need
tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:EnableCompressionInSingleFile=true /property:DebugType=Embedded /property:PublishSingleFile=true --self-contained true
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
files: |
ErsatzTV*.zip
ErsatzTV*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
name: Build & Publish to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Extract Docker Tag
- name: Extract Git Tag
shell: bash shell: bash
run: | run: |
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
- name: Extract Artifacts Version
- name: Set up Docker Buildx Base shell: bash
uses: docker/setup-buildx-action@v1 run: |
id: builder-base tag=$(git describe --tags --abbrev=0)
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
- name: Set up Docker Buildx NVIDIA echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
uses: docker/setup-buildx-action@v1 outputs:
id: builder-nvidia git_tag: ${{ env.GIT_TAG }}
docker_tag: ${{ env.DOCKER_TAG }}
- name: Set up Docker Buildx VAAPI artifacts_version: ${{ env.ARTIFACTS_VERSION }}
uses: docker/setup-buildx-action@v1 info_version: ${{ env.INFO_VERSION }}
id: builder-vaapi build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
- name: Login to DockerHub needs: calculate_version
uses: docker/login-action@v1 with:
with: release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
username: ${{ secrets.DOCKER_HUB_USERNAME }} release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
- name: Build and push base apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
uses: docker/build-push-action@v2 apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
with: ac_username: ${{ secrets.AC_USERNAME }}
builder: ${{ steps.builder-base.outputs.name }} ac_password: ${{ secrets.AC_PASSWORD }}
context: . gh_token: ${{ secrets.GITHUB_TOKEN }}
file: ./docker/Dockerfile build_and_push:
push: true uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
build-args: | needs: calculate_version
INFO_VERSION=${{ env.GIT_TAG }}-docker with:
tags: | base_version: latest
jasongdove/ersatztv:latest info_version: ${{ needs.calculate_version.outputs.git_tag }}
jasongdove/ersatztv:${{ env.DOCKER_TAG }} tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
secrets:
- name: Build and push nvidia docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
uses: docker/build-push-action@v2 docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:latest-nvidia
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:latest-vaapi
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
+3
View File
@@ -0,0 +1,3 @@
[submodule "ErsatzTV-macOS"]
path = ErsatzTV-macOS
url = git@github.com:jasongdove/ErsatzTV-macOS.git
+83 -2
View File
@@ -4,6 +4,82 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
## [0.4.2-alpha] - 2022-02-26
### Fixed
- Add improved but experimental transcoder logic, which can be toggled on and off in `Settings`
- Fix `HLS Segmenter` bug when source video packet contains no duration (`N/A`)
- Fix green line at the bottom of some content scaled using QSV acceleration
### Added
- Add configurable channel group (M3U) and categories (XMLTV)
- Add `Shuffle Schedule Items` option to schedule configuration
- When this is enabled, schedule items will be shuffled rather than looped in order
- **To support this, all playouts will be rebuilt (one time) after upgrading to this version**
### Changed
- Disable framerate normalization by default and on all ffmpeg profiles
- If framerate normalization is desired (not typically needed), it can be re-enabled manually
- Show watermarks over songs
- Hide unused local libraries
## [0.4.1-alpha] - 2022-02-10
### Fixed
- Normalize smart quotes in search queries as they are unsupported by the search library
- Fix incorrect watermark time calculations caused by working ahead in `HLS Segmenter`
- Fix ui crash adding empty path to local library
- Fix ui crash loading collection editor
- Properly flag items as `File Not Found` when local library path (folder) is missing from disk
- Fix playback bug with unknown pixel format
- Fix playback of interlaced mpeg2video on NVIDIA, VAAPI
### Added
- Include `Series` category tag for all episodes in XMLTV
- Include movie, episode (show), music video (artist) genres as `category` tags in XMLTV
- Add framerate normalization to `HLS Segmenter` and `MPEG-TS` streaming modes
- Add `HLS Segmenter Initial Segment Count` setting to allow segmenter to work ahead before allowing client playback
### Changed
- Intermittent watermarks will now fade in and out
- Show collection name in some playout build error messages
- Use hardware-accelerated filter for watermarks on NVIDIA
- Use hardware-accelerated deinterlace for some content on NVIDIA
## [0.4.0-alpha] - 2022-01-29
### Fixed
- Fix m3u `mode` query param to properly override streaming mode for all channels
- `segmenter` for `HLS Segmenter`
- `hls-direct` for `HLS Direct`
- `ts` for `MPEG-TS`
- `ts-legacy` for `MPEG-TS (Legacy)`
- omitting the `mode` parameter returns each channel as configured
- Link `File Not Found` health check to `Trash` page to allow deletion
- Fix `HLS Segmenter` streaming mode with multiple ffmpeg-based clients
- Jellyfin (web) and TiviMate (Android) were specifically tested
### Added
- Hide console window on macOS and Windows; tray menu can be used to access UI, logs and to stop the app
- Also write logs to text files in the `logs` config subfolder
- Add `added_date` to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
- Add `added_inthelast`, `added_notinthelast` search field for relative added date queries
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
## [0.3.8-alpha] - 2022-01-23
### Fixed
- Fix issue preventing some versions of ffmpeg (usually 4.4.x) from streaming MPEG-TS (Legacy) channels at all
- The issue appears to be caused by using a thread count other than `1`
- Thread count is now forced to `1` for all streaming modes other than HLS Segmenter
- Fix bug with HLS Segmenter in cultures where `.` is a group/thousands separator
- Fix search results page crashing with some media kinds
- Always use MPEG-TS or MPEG-TS (Legacy) streaming mode with HDHR (Plex)
- Other configured modes will fall back to MPEG-TS when accessed by Plex
### Changed
- Upgrade ffmpeg from 4.4 to 5.0 in all docker images
- Upgrading from 4.4 to 5.0 is recommended for all installations
## [0.3.7-alpha] - 2022-01-17
### Fixed ### Fixed
- Fix local folder scanners to properly detect removed/re-added folders with unchanged contents - Fix local folder scanners to properly detect removed/re-added folders with unchanged contents
- Fix double-click startup on mac - Fix double-click startup on mac
@@ -909,8 +985,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker. - Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...HEAD [Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.2-alpha...HEAD
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha [0.4.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...v0.4.2-alpha
[0.4.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
[0.3.7-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
[0.3.6-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha [0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha [0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha [0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
+33
View File
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<RootNamespace>ErsatzTV_Windows</RootNamespace>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>Ersatztv.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="Ersatztv.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asmichi.ChildProcess" Version="0.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Compile>
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+14
View File
@@ -0,0 +1,14 @@
namespace ErsatzTV_Windows;
public static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new TrayApplicationContext());
}
}
@@ -0,0 +1,77 @@
using ErsatzTV.Core;
using System.Diagnostics;
using Asmichi.ProcessManagement;
using System.Reflection;
namespace ErsatzTV_Windows;
public class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _trayIcon;
private readonly IChildProcess? _childProcess;
public TrayApplicationContext()
{
_trayIcon = new NotifyIcon
{
Icon = new Icon("./Ersatztv.ico"),
ContextMenuStrip = new ContextMenuStrip(),
Visible = true
};
AddMenuItem("Launch Web UI", LaunchWebUI);
AddMenuItem("Show Logs", ShowLogs);
_trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
AddMenuItem("Exit", Exit);
string folder = AppContext.BaseDirectory;
string exe = Path.Combine(folder, "ErsatzTV.exe");
if (File.Exists(exe))
{
var si = new ChildProcessStartInfo(exe);
_childProcess = ChildProcess.Start(si);
}
}
private void AddMenuItem(string name, EventHandler action)
{
var item = new ToolStripMenuItem(name);
item.Click += action;
_trayIcon.ContextMenuStrip.Items.Add(item);
}
private void LaunchWebUI(object? sender, EventArgs e)
{
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = "http://localhost:8409";
process.Start();
}
private void ShowLogs(object? sender, EventArgs e)
{
if (!Directory.Exists(FileSystemLayout.LogsFolder))
{
Directory.CreateDirectory(FileSystemLayout.LogsFolder);
}
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = FileSystemLayout.LogsFolder;
process.Start();
}
protected override void Dispose(bool disposing)
{
_childProcess?.Dispose();
base.Dispose(disposing);
}
private void Exit(object? sender, EventArgs e)
{
// Hide tray icon, otherwise it will remain shown until user mouses over it
_trayIcon.Visible = false;
Application.Exit();
}
}
+1
Submodule ErsatzTV-macOS added at 2f3ee16f11
@@ -6,6 +6,8 @@ namespace ErsatzTV.Application.Channels
int Id, int Id,
string Number, string Number,
string Name, string Name,
string Group,
string Categories,
int FFmpegProfileId, int FFmpegProfileId,
string Logo, string Logo,
string PreferredLanguageCode, string PreferredLanguageCode,
@@ -9,6 +9,8 @@ namespace ErsatzTV.Application.Channels.Commands
( (
string Name, string Name,
string Number, string Number,
string Group,
string Categories,
int FFmpegProfileId, int FFmpegProfileId,
string Logo, string Logo,
string PreferredLanguageCode, string PreferredLanguageCode,
@@ -27,7 +27,7 @@ namespace ErsatzTV.Application.Channels.Commands
CreateChannel request, CreateChannel request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request); Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c)); return await validation.Apply(c => PersistChannel(dbContext, c));
} }
@@ -65,6 +65,8 @@ namespace ErsatzTV.Application.Channels.Commands
{ {
Name = name, Name = name,
Number = number, Number = number,
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId, FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode, StreamingMode = request.StreamingMode,
Artwork = artwork, Artwork = artwork,
@@ -10,6 +10,8 @@ namespace ErsatzTV.Application.Channels.Commands
int ChannelId, int ChannelId,
string Name, string Name,
string Number, string Number,
string Group,
string Categories,
int FFmpegProfileId, int FFmpegProfileId,
string Logo, string Logo,
string PreferredLanguageCode, string PreferredLanguageCode,
@@ -28,7 +28,7 @@ namespace ErsatzTV.Application.Channels.Commands
UpdateChannel request, UpdateChannel request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request); Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request)); return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
} }
@@ -37,6 +37,8 @@ namespace ErsatzTV.Application.Channels.Commands
{ {
c.Name = update.Name; c.Name = update.Name;
c.Number = update.Number; c.Number = update.Number;
c.Group = update.Group;
c.Categories = update.Categories;
c.FFmpegProfileId = update.FFmpegProfileId; c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredLanguageCode = update.PreferredLanguageCode; c.PreferredLanguageCode = update.PreferredLanguageCode;
c.Artwork ??= new List<Artwork>(); c.Artwork ??= new List<Artwork>();
+2
View File
@@ -11,6 +11,8 @@ namespace ErsatzTV.Application.Channels
channel.Id, channel.Id,
channel.Number, channel.Number,
channel.Name, channel.Name,
channel.Group,
channel.Categories,
channel.FFmpegProfileId, channel.FFmpegProfileId,
GetLogo(channel), GetLogo(channel),
channel.PreferredLanguageCode, channel.PreferredLanguageCode,
@@ -0,0 +1,6 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries;
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries;
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<GetChannelFramerateHandler> _logger;
public GetChannelFramerateHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<GetChannelFramerateHandler> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
_logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
return None;
}
private int ParseFrameRate(string frameRate)
{
if (!int.TryParse(frameRate, out int fr))
{
string[] split = (frameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
fr = 24;
}
}
return fr;
}
}
@@ -36,10 +36,14 @@ namespace ErsatzTV.Application.Channels.Queries
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect; channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel); result.Add(channel);
break; break;
case "ts": case "ts-legacy":
channel.StreamingMode = StreamingMode.TransportStream; channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel); result.Add(channel);
break; break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default: default:
result.Add(channel); result.Add(channel);
break; break;
@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="9.0.0" /> <PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -24,5 +24,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>; bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
} }
@@ -58,7 +58,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
NormalizeLoudness = request.NormalizeLoudness, NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels, AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate, AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio NormalizeAudio = request.NormalizeAudio,
NormalizeFramerate = request.NormalizeFramerate
}); });
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) => private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
@@ -25,5 +25,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>; bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
} }
@@ -50,6 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
p.AudioChannels = update.AudioChannels; p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate; p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.Transcode && update.NormalizeAudio; p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id); return new UpdateFFmpegProfileResult(p.Id);
} }
@@ -85,6 +85,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder); Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
} }
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegUseExperimentalTranscoder,
request.Settings.UseExperimentalTranscoder.ToString());
await _configElementRepository.Upsert( await _configElementRepository.Upsert(
ConfigElementKey.FFmpegPreferredLanguageCode, ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredLanguageCode); request.Settings.PreferredLanguageCode);
@@ -119,6 +123,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
ConfigElementKey.FFmpegWorkAheadSegmenters, ConfigElementKey.FFmpegWorkAheadSegmenters,
request.Settings.WorkAheadSegmenterLimit); request.Settings.WorkAheadSegmenterLimit);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegInitialSegmentCount,
request.Settings.InitialSegmentCount);
return Unit.Default; return Unit.Default;
} }
} }
@@ -23,5 +23,6 @@ namespace ErsatzTV.Application.FFmpegProfiles
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, int AudioSampleRate,
bool NormalizeAudio); bool NormalizeAudio,
bool NormalizeFramerate);
} }
@@ -11,5 +11,7 @@
public int? GlobalFallbackFillerId { get; set; } public int? GlobalFallbackFillerId { get; set; }
public int HlsSegmenterIdleTimeout { get; set; } public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; } public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
public bool UseExperimentalTranscoder { get; set; }
} }
} }
@@ -25,7 +25,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
profile.NormalizeLoudness, profile.NormalizeLoudness,
profile.AudioChannels, profile.AudioChannels,
profile.AudioSampleRate, profile.AudioSampleRate,
profile.NormalizeAudio); profile.NormalizeAudio,
profile.NormalizeVideo && profile.NormalizeFramerate);
private static ResolutionViewModel Project(Resolution resolution) => private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
@@ -34,6 +34,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout); await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit = Option<int> workAheadSegmenterLimit =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters); await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
Option<bool> useExperimentalTranscoder =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseExperimentalTranscoder);
var result = new FFmpegSettingsViewModel var result = new FFmpegSettingsViewModel
{ {
@@ -44,6 +48,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"), PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60), HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1), WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1),
UseExperimentalTranscoder = await useExperimentalTranscoder.IfNoneAsync(false)
}; };
foreach (int watermarkId in watermark) foreach (int watermarkId in watermark)
@@ -3,5 +3,5 @@ using MediatR;
namespace ErsatzTV.Application.Libraries.Queries namespace ErsatzTV.Application.Libraries.Queries
{ {
public record GetAllLibraries : IRequest<List<LibraryViewModel>>; public record GetConfiguredLibraries : IRequest<List<LibraryViewModel>>;
} }
@@ -10,13 +10,16 @@ using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries namespace ErsatzTV.Application.Libraries.Queries
{ {
public class GetAllLibrariesHandler : IRequestHandler<GetAllLibraries, List<LibraryViewModel>> public class GetConfiguredLibrariesHandler : IRequestHandler<GetConfiguredLibraries, List<LibraryViewModel>>
{ {
private readonly ILibraryRepository _libraryRepository; private readonly ILibraryRepository _libraryRepository;
public GetAllLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository; public GetConfiguredLibrariesHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<List<LibraryViewModel>> Handle(GetAllLibraries request, CancellationToken cancellationToken) => public Task<List<LibraryViewModel>> Handle(
GetConfiguredLibraries request,
CancellationToken cancellationToken) =>
_libraryRepository.GetAll() _libraryRepository.GetAll()
.Map( .Map(
list => list.Filter(ShouldIncludeLibrary) list => list.Filter(ShouldIncludeLibrary)
@@ -28,7 +31,7 @@ namespace ErsatzTV.Application.Libraries.Queries
private static bool ShouldIncludeLibrary(Library library) => private static bool ShouldIncludeLibrary(Library library) =>
library switch library switch
{ {
LocalLibrary => true, LocalLibrary => library.Paths.Count > 0,
PlexLibrary plex => plex.ShouldSyncItems, PlexLibrary plex => plex.ShouldSyncItems,
JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems, JellyfinLibrary jellyfin => jellyfin.ShouldSyncItems,
EmbyLibrary emby => emby.ShouldSyncItems, EmbyLibrary emby => emby.ShouldSyncItems,
@@ -47,6 +47,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
.ThenInclude(i => (i as Movie).MovieMetadata) .ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => (i as Movie).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Artist).ArtistMetadata) .ThenInclude(i => (i as Artist).ArtistMetadata)
.ThenInclude(mvm => mvm.Artwork) .ThenInclude(mvm => mvm.Artwork)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
@@ -56,6 +59,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
.ThenInclude(i => (i as MusicVideo).Artist) .ThenInclude(i => (i as MusicVideo).Artist)
.ThenInclude(a => a.ArtistMetadata) .ThenInclude(a => a.ArtistMetadata)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => (i as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Show).ShowMetadata) .ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
@@ -81,11 +87,20 @@ namespace ErsatzTV.Application.MediaCards.Queries
.ThenInclude(i => (i as Episode).Season) .ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.SeasonMetadata) .ThenInclude(s => s.SeasonMetadata)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => (i as Episode).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as OtherVideo).OtherVideoMetadata) .ThenInclude(i => (i as OtherVideo).OtherVideoMetadata)
.ThenInclude(ovm => ovm.Artwork) .ThenInclude(ovm => ovm.Artwork)
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.ThenInclude(i => (i as OtherVideo).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Song).SongMetadata) .ThenInclude(i => (i as Song).SongMetadata)
.ThenInclude(ovm => ovm.Artwork) .ThenInclude(ovm => ovm.Artwork)
.Include(c => c.MediaItems)
.ThenInclude(i => (i as Song).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.SelectOneAsync(c => c.Id, c => c.Id == request.Id) .SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.Map(c => c.ToEither(BaseError.New("Unable to load collection"))) .Map(c => c.ToEither(BaseError.New("Unable to load collection")))
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby)); .MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
@@ -21,7 +21,7 @@ namespace ErsatzTV.Application.MediaCollections.Queries
GetCollectionById request, GetCollectionById request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Collections return await dbContext.Collections
.SelectOneAsync(c => c.Id, c => c.Id == request.Id) .SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.MapT(ProjectToViewModel); .MapT(ProjectToViewModel);
@@ -8,5 +8,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
public record CreateProgramSchedule( public record CreateProgramSchedule(
string Name, string Name,
bool KeepMultiPartEpisodesTogether, bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, CreateProgramScheduleResult>>; bool TreatCollectionsAsShows,
bool ShuffleScheduleItems) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
} }
@@ -50,7 +50,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
{ {
Name = name, Name = name,
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether, KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows,
ShuffleScheduleItems = request.ShuffleScheduleItems
}; };
}); });
@@ -10,5 +10,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
int ProgramScheduleId, int ProgramScheduleId,
string Name, string Name,
bool KeepMultiPartEpisodesTogether, bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>; bool TreatCollectionsAsShows,
bool ShuffleScheduleItems) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
} }
@@ -31,7 +31,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
UpdateProgramSchedule request, UpdateProgramSchedule request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request); Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request)); return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
@@ -45,12 +45,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
// we need to rebuild playouts if the playback order or keep multi-episodes has been modified // we need to rebuild playouts if the playback order or keep multi-episodes has been modified
bool needToRebuildPlayout = bool needToRebuildPlayout =
programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether || programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether ||
programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows; programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows ||
programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems;
programSchedule.Name = request.Name; programSchedule.Name = request.Name;
programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether;
programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether && programSchedule.TreatCollectionsAsShows = programSchedule.KeepMultiPartEpisodesTogether &&
request.TreatCollectionsAsShows; request.TreatCollectionsAsShows;
programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems;
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
@@ -10,7 +10,8 @@ namespace ErsatzTV.Application.ProgramSchedules
programSchedule.Id, programSchedule.Id,
programSchedule.Name, programSchedule.Name,
programSchedule.KeepMultiPartEpisodesTogether, programSchedule.KeepMultiPartEpisodesTogether,
programSchedule.TreatCollectionsAsShows); programSchedule.TreatCollectionsAsShows,
programSchedule.ShuffleScheduleItems);
internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) => internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) =>
programScheduleItem switch programScheduleItem switch
@@ -4,5 +4,6 @@
int Id, int Id,
string Name, string Name,
bool KeepMultiPartEpisodesTogether, bool KeepMultiPartEpisodesTogether,
bool TreatCollectionsAsShows); bool TreatCollectionsAsShows,
bool ShuffleScheduleItems);
} }
@@ -25,7 +25,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
ps.Id, ps.Id,
ps.Name, ps.Name,
ps.KeepMultiPartEpisodesTogether, ps.KeepMultiPartEpisodesTogether,
ps.TreatCollectionsAsShows)) ps.TreatCollectionsAsShows,
ps.ShuffleScheduleItems))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
} }
@@ -21,7 +21,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
GetProgramScheduleById request, GetProgramScheduleById request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ProgramSchedules return await dbContext.ProgramSchedules
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id) .SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
.MapT(ProjectToViewModel); .MapT(ProjectToViewModel);
@@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -23,7 +24,11 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
GetProgramScheduleItems request, GetProgramScheduleItems request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<ProgramSchedule> maybeProgramSchedule =
await dbContext.ProgramSchedules.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id);
return await dbContext.ProgramScheduleItems return await dbContext.ProgramScheduleItems
.Filter(psi => psi.ProgramScheduleId == request.Id) .Filter(psi => psi.ProgramScheduleId == request.Id)
.Include(i => i.Collection) .Include(i => i.Collection)
@@ -51,7 +56,29 @@ namespace ErsatzTV.Application.ProgramSchedules.Queries
.Include(i => i.TailFiller) .Include(i => i.TailFiller)
.Include(i => i.FallbackFiller) .Include(i => i.FallbackFiller)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel).ToList()); .Map(
programScheduleItems => programScheduleItems.Map(ProjectToViewModel)
.Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList());
}
// shuffled schedule items supports a limited set of properly values
private ProgramScheduleItemViewModel EnforceProperties(
Option<ProgramSchedule> maybeProgramSchedule,
ProgramScheduleItemViewModel item)
{
foreach (ProgramSchedule programSchedule in maybeProgramSchedule)
{
if (programSchedule.ShuffleScheduleItems)
{
item = item with { StartType = StartType.Dynamic };
if (item.PlayoutMode == PlayoutMode.Flood)
{
item = item with { PlayoutMode = PlayoutMode.One };
}
}
}
return item;
} }
} }
} }
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors; using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@@ -21,6 +22,7 @@ namespace ErsatzTV.Application.Streaming.Commands
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
public StartFFmpegSessionHandler( public StartFFmpegSessionHandler(
@@ -28,13 +30,15 @@ namespace ErsatzTV.Application.Streaming.Commands
ILogger<StartFFmpegSessionHandler> logger, ILogger<StartFFmpegSessionHandler> logger,
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
IFFmpegSegmenterService ffmpegSegmenterService, IFFmpegSegmenterService ffmpegSegmenterService,
IConfigElementRepository configElementRepository) IConfigElementRepository configElementRepository,
IHlsPlaylistFilter hlsPlaylistFilter)
{ {
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_logger = logger; _logger = logger;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_ffmpegSegmenterService = ffmpegSegmenterService; _ffmpegSegmenterService = ffmpegSegmenterService;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_hlsPlaylistFilter = hlsPlaylistFilter;
} }
public Task<Either<BaseError, Unit>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) => public Task<Either<BaseError, Unit>> Handle(StartFFmpegSession request, CancellationToken cancellationToken) =>
@@ -50,7 +54,7 @@ namespace ErsatzTV.Application.Streaming.Commands
TimeSpan idleTimeout = await _configElementRepository TimeSpan idleTimeout = await _configElementRepository
.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout) .GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout)
.Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1))); .Map(maybeTimeout => maybeTimeout.Match(i => TimeSpan.FromSeconds(i), () => TimeSpan.FromMinutes(1)));
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService<HlsSessionWorker>(); HlsSessionWorker worker = scope.ServiceProvider.GetRequiredService<HlsSessionWorker>();
_ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker); _ffmpegSegmenterService.SessionWorkers.AddOrUpdate(request.ChannelNumber, _ => worker, (_, _) => worker);
@@ -68,12 +72,32 @@ namespace ErsatzTV.Application.Streaming.Commands
request.ChannelNumber, request.ChannelNumber,
"live.m3u8"); "live.m3u8");
IConfigElementRepository repo = scope.ServiceProvider.GetRequiredService<IConfigElementRepository>();
int initialSegmentCount = await repo.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount)
.Map(maybeCount => maybeCount.Match(identity, () => 1));
await WaitForPlaylistSegments(playlistFileName, initialSegmentCount, worker);
return Unit.Default;
}
private async Task WaitForPlaylistSegments(string playlistFileName, int initialSegmentCount, IHlsSessionWorker worker)
{
while (!File.Exists(playlistFileName)) while (!File.Exists(playlistFileName))
{ {
await Task.Delay(TimeSpan.FromMilliseconds(100)); await Task.Delay(TimeSpan.FromMilliseconds(100));
} }
return Unit.Default; var segmentCount = 0;
while (segmentCount < initialSegmentCount)
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
DateTimeOffset now = DateTimeOffset.Now.AddSeconds(-30);
string[] input = await File.ReadAllLinesAsync(playlistFileName);
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(worker.PlaylistStart, now, input);
segmentCount = result.SegmentCount;
}
} }
private Task<Validation<BaseError, Unit>> Validate(StartFFmpegSession request) => private Task<Validation<BaseError, Unit>> Validate(StartFFmpegSession request) =>
@@ -1,9 +1,11 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using ErsatzTV.Application.Channels.Queries;
using ErsatzTV.Application.Streaming.Queries; using ErsatzTV.Application.Streaming.Queries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@@ -22,6 +24,7 @@ namespace ErsatzTV.Application.Streaming
public class HlsSessionWorker : IHlsSessionWorker public class HlsSessionWorker : IHlsSessionWorker
{ {
private static int _workAheadCount; private static int _workAheadCount;
private readonly IHlsPlaylistFilter _hlsPlaylistFilter;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<HlsSessionWorker> _logger; private readonly ILogger<HlsSessionWorker> _logger;
private DateTimeOffset _lastAccess; private DateTimeOffset _lastAccess;
@@ -29,9 +32,11 @@ namespace ErsatzTV.Application.Streaming
private Timer _timer; private Timer _timer;
private readonly object _sync = new(); private readonly object _sync = new();
private DateTimeOffset _playlistStart; private DateTimeOffset _playlistStart;
private Option<int> _targetFramerate;
public HlsSessionWorker(IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger) public HlsSessionWorker(IHlsPlaylistFilter hlsPlaylistFilter, IServiceScopeFactory serviceScopeFactory, ILogger<HlsSessionWorker> logger)
{ {
_hlsPlaylistFilter = hlsPlaylistFilter;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_logger = logger; _logger = logger;
} }
@@ -65,6 +70,13 @@ namespace ErsatzTV.Application.Streaming
CancellationToken cancellationToken = cts.Token; CancellationToken cancellationToken = cts.Token;
_logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber); _logger.LogInformation("Starting HLS session for channel {Channel}", channelNumber);
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
_targetFramerate = await mediator.Send(
new GetChannelFramerate(channelNumber),
cancellationToken);
Touch(); Touch();
_transcodedUntil = DateTimeOffset.Now; _transcodedUntil = DateTimeOffset.Now;
@@ -113,7 +125,11 @@ namespace ErsatzTV.Application.Streaming
} }
} }
private async Task<bool> Transcode(string channelNumber, bool firstProcess, bool realtime, CancellationToken cancellationToken) private async Task<bool> Transcode(
string channelNumber,
bool firstProcess,
bool realtime,
CancellationToken cancellationToken)
{ {
try try
{ {
@@ -132,12 +148,17 @@ namespace ErsatzTV.Application.Streaming
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>(); IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
long ptsOffset = await GetPtsOffset(mediator, channelNumber, cancellationToken);
// _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset);
var request = new GetPlayoutItemProcessByChannelNumber( var request = new GetPlayoutItemProcessByChannelNumber(
channelNumber, channelNumber,
"segmenter", "segmenter",
firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
!firstProcess, !firstProcess,
realtime); realtime,
ptsOffset,
_targetFramerate);
// _logger.LogInformation("Request {@Request}", request); // _logger.LogInformation("Request {@Request}", request);
@@ -197,7 +218,7 @@ namespace ErsatzTV.Application.Streaming
return true; return true;
} }
private async Task TrimAndDelete(string channelNumber, CancellationToken cancellationToken) private async Task TrimAndDelete(string channelNumber, CancellationToken cancellationToken)
{ {
string playlistFileName = Path.Combine( string playlistFileName = Path.Combine(
@@ -209,29 +230,66 @@ namespace ErsatzTV.Application.Streaming
{ {
// trim playlist and insert discontinuity before appending with new ffmpeg process // trim playlist and insert discontinuity before appending with new ffmpeg process
string[] lines = await File.ReadAllLinesAsync(playlistFileName, cancellationToken); string[] lines = await File.ReadAllLinesAsync(playlistFileName, cancellationToken);
TrimPlaylistResult trimResult = HlsPlaylistFilter.TrimPlaylistWithDiscontinuity( TrimPlaylistResult trimResult = _hlsPlaylistFilter.TrimPlaylistWithDiscontinuity(
_playlistStart, _playlistStart,
DateTimeOffset.Now.AddMinutes(-1), DateTimeOffset.Now.AddMinutes(-1),
lines); lines);
await File.WriteAllTextAsync(playlistFileName, trimResult.Playlist, cancellationToken); await File.WriteAllTextAsync(playlistFileName, trimResult.Playlist, cancellationToken);
// delete old segments // delete old segments
foreach (string file in Directory.GetFiles( var allSegments = Directory.GetFiles(
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber), Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber),
"*.ts")) "live*.ts")
.Map(
file =>
{
string fileName = Path.GetFileName(file);
var sequenceNumber = int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]);
return new Segment(file, sequenceNumber);
})
.ToList();
var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList();
// if (toDelete.Count > 0)
// {
// _logger.LogInformation(
// "Deleting HLS segments {Min} to {Max} (less than {StartSequence})",
// toDelete.Map(s => s.SequenceNumber).Min(),
// toDelete.Map(s => s.SequenceNumber).Max(),
// trimResult.Sequence);
// }
foreach (Segment segment in toDelete)
{ {
string fileName = Path.GetFileName(file); File.Delete(segment.File);
if (fileName.StartsWith("live") && int.Parse(fileName.Replace("live", string.Empty).Split('.')[0]) <
trimResult.Sequence)
{
File.Delete(file);
}
} }
_playlistStart = trimResult.PlaylistStart; _playlistStart = trimResult.PlaylistStart;
} }
} }
private async Task<long> GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken)
{
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber));
Option<FileInfo> lastSegment =
Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault());
long result = 0;
foreach (FileInfo segment in lastSegment)
{
Either<BaseError, PtsAndDuration> queryResult = await mediator.Send(
new GetLastPtsDuration(segment.FullName),
cancellationToken);
foreach (PtsAndDuration ptsAndDuration in queryResult.RightToSeq())
{
result = ptsAndDuration.Pts + ptsAndDuration.Duration;
}
}
return result;
}
private async Task<int> GetWorkAheadLimit() private async Task<int> GetWorkAheadLimit()
{ {
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
@@ -239,5 +297,7 @@ namespace ErsatzTV.Application.Streaming
return await repo.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters) return await repo.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters)
.Map(maybeCount => maybeCount.Match(identity, () => 1)); .Map(maybeCount => maybeCount.Match(identity, () => 1));
} }
private record Segment(string File, int SequenceNumber);
} }
} }
@@ -0,0 +1,16 @@
namespace ErsatzTV.Application.Streaming;
public record PtsAndDuration(long Pts, long Duration)
{
public static PtsAndDuration From(string ffprobeLine)
{
string[] split = ffprobeLine.Split("|");
var left = long.Parse(split[0]);
if (!long.TryParse(split[1], out long right))
{
// some durations are N/A, so we have to guess at something
right = 10_000;
}
return new PtsAndDuration(left, right);
}
}
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Streaming.Queries
string Mode, string Mode,
DateTimeOffset Now, DateTimeOffset Now,
bool StartAtZero, bool StartAtZero,
bool HlsRealtime) : IRequest<Either<BaseError, PlayoutItemProcessModel>>; bool HlsRealtime,
long PtsOffset) : IRequest<Either<BaseError, PlayoutItemProcessModel>>;
} }
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.Streaming.Queries
"ts-legacy", "ts-legacy",
DateTimeOffset.Now, DateTimeOffset.Now,
false, false,
true) true,
0)
{ {
Scheme = scheme; Scheme = scheme;
Host = host; Host = host;
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
@@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries
{ {
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber> public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
{ {
private readonly IFFmpegProcessService _ffmpegProcessService; private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
public GetConcatProcessByChannelNumberHandler( public GetConcatProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService) IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
: base(dbContextFactory) : base(dbContextFactory)
{ {
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
} }
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess( protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
@@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.ConcatChannel( IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
Process process = ffmpegProcessService.ConcatChannel(
ffmpegPath, ffmpegPath,
saveReports, saveReports,
channel, channel,
@@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Streaming.Queries;
public record GetLastPtsDuration(string FileName) : IRequest<Either<BaseError, PtsAndDuration>>;
@@ -0,0 +1,87 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Streaming.Queries;
public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Either<BaseError, PtsAndDuration>>
{
private readonly IConfigElementRepository _configElementRepository;
public GetLastPtsDurationHandler(IConfigElementRepository configElementRepository)
{
_configElementRepository = configElementRepository;
}
public async Task<Either<BaseError, PtsAndDuration>> Handle(
GetLastPtsDuration request,
CancellationToken cancellationToken)
{
Validation<BaseError, RequestParameters> validation = await Validate(request);
return await validation.Match(
Handle,
error => Task.FromResult<Either<BaseError, PtsAndDuration>>(error.Join()));
}
private async Task<Validation<BaseError, RequestParameters>> Validate(GetLastPtsDuration request) =>
await ValidateFFprobePath()
.MapT(
ffprobePath => new RequestParameters(
request.FileName,
ffprobePath));
private async Task<Either<BaseError, PtsAndDuration>> Handle(RequestParameters parameters)
{
var startInfo = new ProcessStartInfo
{
FileName = parameters.FFprobePath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add("0");
startInfo.ArgumentList.Add("-show_entries");
startInfo.ArgumentList.Add("packet=pts,duration");
startInfo.ArgumentList.Add("-of");
startInfo.ArgumentList.Add("compact=p=0:nk=1");
startInfo.ArgumentList.Add("-read_intervals");
startInfo.ArgumentList.Add("-999999");
startInfo.ArgumentList.Add(parameters.FileName);
var probe = new Process
{
StartInfo = startInfo
};
probe.Start();
return await probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, PtsAndDuration>>(
async output =>
{
await probe.WaitForExitAsync();
return probe.ExitCode == 0
? PtsAndDuration.From(output.Split("\n").Filter(s => !string.IsNullOrWhiteSpace(s)).Last().Trim())
: BaseError.New($"FFprobe at {parameters.FFprobePath} exited with code {probe.ExitCode}");
});
}
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(string FileName, string FFprobePath);
}
@@ -1,21 +1,18 @@
using System; using System;
using LanguageExt;
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public record GetPlayoutItemProcessByChannelNumber : FFmpegProcessRequest public record GetPlayoutItemProcessByChannelNumber(string ChannelNumber,
{ string Mode,
public GetPlayoutItemProcessByChannelNumber( DateTimeOffset Now,
string channelNumber, bool StartAtZero,
string mode, bool HlsRealtime,
DateTimeOffset now, long PtsOffset,
bool startAtZero, Option<int> TargetFramerate) : FFmpegProcessRequest(ChannelNumber,
bool hlsRealtime) : base( Mode,
channelNumber, Now,
mode, StartAtZero,
now, HlsRealtime,
startAtZero, PtsOffset);
hlsRealtime)
{
}
}
} }
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Errors; using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Jellyfin;
@@ -30,15 +31,15 @@ namespace ErsatzTV.Application.Streaming.Queries
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService; private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly IPlexPathReplacementService _plexPathReplacementService; private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly ISongVideoGenerator _songVideoGenerator; private readonly ISongVideoGenerator _songVideoGenerator;
public GetPlayoutItemProcessByChannelNumberHandler( public GetPlayoutItemProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService, IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService, IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService, IJellyfinPathReplacementService jellyfinPathReplacementService,
@@ -49,7 +50,7 @@ namespace ErsatzTV.Application.Streaming.Queries
ISongVideoGenerator songVideoGenerator) ISongVideoGenerator songVideoGenerator)
: base(dbContextFactory) : base(dbContextFactory)
{ {
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService; _plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService; _jellyfinPathReplacementService = jellyfinPathReplacementService;
@@ -111,6 +112,8 @@ namespace ErsatzTV.Application.Streaming.Queries
maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now); maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now);
} }
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
return await maybePlayoutItem.Match( return await maybePlayoutItem.Match(
async playoutItemWithPath => async playoutItemWithPath =>
{ {
@@ -141,7 +144,7 @@ namespace ErsatzTV.Application.Streaming.Queries
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
Process process = await _ffmpegProcessService.ForPlayoutItem( Process process = await ffmpegProcessService.ForPlayoutItem(
ffmpegPath, ffmpegPath,
saveReports, saveReports,
channel, channel,
@@ -158,7 +161,9 @@ namespace ErsatzTV.Application.Streaming.Queries
request.HlsRealtime, request.HlsRealtime,
playoutItemWithPath.PlayoutItem.FillerKind, playoutItemWithPath.PlayoutItem.FillerKind,
playoutItemWithPath.PlayoutItem.InPoint, playoutItemWithPath.PlayoutItem.InPoint,
playoutItemWithPath.PlayoutItem.OutPoint); playoutItemWithPath.PlayoutItem.OutPoint,
request.PtsOffset,
request.TargetFramerate);
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset); var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);
@@ -188,12 +193,13 @@ namespace ErsatzTV.Application.Streaming.Queries
case UnableToLocatePlayoutItem: case UnableToLocatePlayoutItem:
if (channel.FFmpegProfile.Transcode) if (channel.FFmpegProfile.Transcode)
{ {
Process errorProcess = await _ffmpegProcessService.ForError( Process errorProcess = await ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
maybeDuration, maybeDuration,
"Channel is Offline", "Channel is Offline",
request.HlsRealtime); request.HlsRealtime,
request.PtsOffset);
return new PlayoutItemProcessModel(errorProcess, finish); return new PlayoutItemProcessModel(errorProcess, finish);
} }
@@ -207,12 +213,13 @@ namespace ErsatzTV.Application.Streaming.Queries
case PlayoutItemDoesNotExistOnDisk: case PlayoutItemDoesNotExistOnDisk:
if (channel.FFmpegProfile.Transcode) if (channel.FFmpegProfile.Transcode)
{ {
Process errorProcess = await _ffmpegProcessService.ForError( Process errorProcess = await ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
maybeDuration, maybeDuration,
error.Value, error.Value,
request.HlsRealtime); request.HlsRealtime,
request.PtsOffset);
return new PlayoutItemProcessModel(errorProcess, finish); return new PlayoutItemProcessModel(errorProcess, finish);
} }
@@ -226,12 +233,13 @@ namespace ErsatzTV.Application.Streaming.Queries
default: default:
if (channel.FFmpegProfile.Transcode) if (channel.FFmpegProfile.Transcode)
{ {
Process errorProcess = await _ffmpegProcessService.ForError( Process errorProcess = await ffmpegProcessService.ForError(
ffmpegPath, ffmpegPath,
channel, channel,
maybeDuration, maybeDuration,
"Channel is Offline", "Channel is Offline",
request.HlsRealtime); request.HlsRealtime,
request.PtsOffset);
return new PlayoutItemProcessModel(errorProcess, finish); return new PlayoutItemProcessModel(errorProcess, finish);
} }
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.Streaming.Queries
"ts", "ts",
DateTimeOffset.Now, DateTimeOffset.Now,
false, false,
true) true,
0)
{ {
Scheme = scheme; Scheme = scheme;
Host = host; Host = host;
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
@@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries
{ {
public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber> public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber>
{ {
private readonly IFFmpegProcessService _ffmpegProcessService; private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
public GetWrappedProcessByChannelNumberHandler( public GetWrappedProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService) IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
: base(dbContextFactory) : base(dbContextFactory)
{ {
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
} }
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess( protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
@@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports) .GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false)); .Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.WrapSegmenter( IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
Process process = ffmpegProcessService.WrapSegmenter(
ffmpegPath, ffmpegPath,
saveReports, saveReports,
channel, channel,
@@ -1,5 +1,6 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
@@ -10,8 +11,8 @@ namespace ErsatzTV.Application.Watermarks.Commands
string Image, string Image,
ChannelWatermarkMode Mode, ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource, ChannelWatermarkImageSource ImageSource,
ChannelWatermarkLocation Location, WatermarkLocation Location,
ChannelWatermarkSize Size, WatermarkSize Size,
int Width, int Width,
int HorizontalMargin, int HorizontalMargin,
int VerticalMargin, int VerticalMargin,
@@ -1,5 +1,6 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
@@ -11,8 +12,8 @@ namespace ErsatzTV.Application.Watermarks.Commands
string Image, string Image,
ChannelWatermarkMode Mode, ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource, ChannelWatermarkImageSource ImageSource,
ChannelWatermarkLocation Location, WatermarkLocation Location,
ChannelWatermarkSize Size, WatermarkSize Size,
int Width, int Width,
int HorizontalMargin, int HorizontalMargin,
int VerticalMargin, int VerticalMargin,
@@ -21,7 +21,7 @@ namespace ErsatzTV.Application.Watermarks.Commands
UpdateWatermark request, UpdateWatermark request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ChannelWatermark> validation = await Validate(dbContext, request); Validation<BaseError, ChannelWatermark> validation = await Validate(dbContext, request);
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request)); return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
} }
@@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
namespace ErsatzTV.Application.Watermarks namespace ErsatzTV.Application.Watermarks
{ {
@@ -8,8 +9,8 @@ namespace ErsatzTV.Application.Watermarks
string Name, string Name,
ChannelWatermarkMode Mode, ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource, ChannelWatermarkImageSource ImageSource,
ChannelWatermarkLocation Location, WatermarkLocation Location,
ChannelWatermarkSize Size, WatermarkSize Size,
int Width, int Width,
int HorizontalMargin, int HorizontalMargin,
int VerticalMargin, int VerticalMargin,
@@ -6,9 +6,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.3.0" /> <PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" /> <PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -16,7 +19,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.FFmpeg.State;
using FluentAssertions; using FluentAssertions;
using LanguageExt; using LanguageExt;
using NUnit.Framework; using NUnit.Framework;
@@ -140,7 +142,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
false, false,
false, false,
ChannelWatermarkLocation.BottomLeft, WatermarkLocation.BottomLeft,
false, false,
100, 100,
"[0:0][1:v]overlay=x=134:y=H-h-54[v]", "[0:0][1:v]overlay=x=134:y=H-h-54[v]",
@@ -150,7 +152,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
false, false,
false, false,
ChannelWatermarkLocation.BottomRight, WatermarkLocation.BottomRight,
false, false,
100, 100,
"[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]", "[0:0][1:v]overlay=x=W-w-134:y=H-h-54[v]",
@@ -160,7 +162,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
false, false,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
false, false,
100, 100,
"[0:0][1:v]overlay=x=134:y=54[v]", "[0:0][1:v]overlay=x=134:y=54[v]",
@@ -170,7 +172,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
false, false,
false, false,
ChannelWatermarkLocation.TopRight, WatermarkLocation.TopRight,
false, false,
100, 100,
"[0:0][1:v]overlay=x=W-w-134:y=54[v]", "[0:0][1:v]overlay=x=W-w-134:y=54[v]",
@@ -180,17 +182,17 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
false, false,
true, true,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
false, false,
100, 100,
"[0:0][1:v]overlay=x=134:y=54:enable='lt(mod(mod(time(0),60*60),10*60),15)'[v]", "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)'[wmp];[0:0][wmp]overlay=x=134:y=54,format=nv12[v]",
"0:1", "0:1",
"[v]")] "[v]")]
[TestCase( [TestCase(
false, false,
false, false,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
true, true,
100, 100,
"[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]", "[1:v]scale=384:-1[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
@@ -200,7 +202,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
false, false,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
false, false,
90, 90,
"[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]", "[1:v]format=yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8,colorchannelmixer=aa=0.90[wmp];[0:0][wmp]overlay=x=134:y=54[v]",
@@ -210,7 +212,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
true, true,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
false, false,
100, 100,
"[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
@@ -220,7 +222,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
true, true,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
true, true,
100, 100,
"[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]", "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
@@ -230,7 +232,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
true, true,
true, true,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
false, false,
100, 100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]", "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
@@ -240,7 +242,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
true, true,
false, false,
false, false,
ChannelWatermarkLocation.TopLeft, WatermarkLocation.TopLeft,
false, false,
100, 100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]", "[0:1]apad=whole_dur=3300000ms[a];[0:0][1:v]overlay=x=134:y=54[v]",
@@ -250,30 +252,43 @@ namespace ErsatzTV.Core.Tests.FFmpeg
bool alignAudio, bool alignAudio,
bool deinterlace, bool deinterlace,
bool intermittent, bool intermittent,
ChannelWatermarkLocation location, WatermarkLocation location,
bool scaled, bool scaled,
int opacity, int opacity,
string expectedVideoFilter, string expectedVideoFilter,
string expectedAudioLabel, string expectedAudioLabel,
string expectedVideoLabel) string expectedVideoLabel)
{ {
var watermark = new ChannelWatermark
{
Mode = intermittent
? ChannelWatermarkMode.Intermittent
: ChannelWatermarkMode.Permanent,
DurationSeconds = intermittent ? 15 : 0,
FrequencyMinutes = intermittent ? 10 : 0,
Location = location,
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize,
WidthPercent = scaled ? 20 : 0,
Opacity = opacity,
HorizontalMarginPercent = 7,
VerticalMarginPercent = 5
};
Option<List<FadePoint>> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent
? Some(
WatermarkCalculator.CalculateFadePoints(
new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)),
TimeSpan.Zero,
TimeSpan.FromMinutes(55),
TimeSpan.Zero,
watermark.FrequencyMinutes,
watermark.DurationSeconds))
: None;
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder() FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithWatermark( .WithWatermark(
Some( Some(watermark),
new ChannelWatermark maybeFadePoints,
{
Mode = intermittent
? ChannelWatermarkMode.Intermittent
: ChannelWatermarkMode.Permanent,
DurationSeconds = intermittent ? 15 : 0,
FrequencyMinutes = intermittent ? 10 : 0,
Location = location,
Size = scaled ? ChannelWatermarkSize.Scaled : ChannelWatermarkSize.ActualSize,
WidthPercent = scaled ? 20 : 0,
Opacity = opacity,
HorizontalMarginPercent = 7,
VerticalMarginPercent = 5
}),
new Resolution { Width = 1920, Height = 1080 }, new Resolution { Width = 1920, Height = 1080 },
None) None)
.WithDeinterlace(deinterlace) .WithDeinterlace(deinterlace)
@@ -290,6 +305,213 @@ namespace ErsatzTV.Core.Tests.FFmpeg
filter.VideoLabel.Should().Be(expectedVideoLabel); filter.VideoLabel.Should().Be(expectedVideoLabel);
}); });
} }
[Test]
[TestCase(
false,
false,
false,
WatermarkLocation.BottomLeft,
false,
100,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
false,
WatermarkLocation.BottomLeft,
false,
100,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=H-h-54,hwupload[v]",
"0:1",
"[v]",
true)]
[TestCase(
false,
false,
true,
WatermarkLocation.TopLeft,
false,
100,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
true,
WatermarkLocation.TopLeft,
false,
100,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,fade=in:st=300:d=1:alpha=1:enable='between(t,0,314)',fade=out:st=315:d=1:alpha=1:enable='between(t,301,899)',fade=in:st=900:d=1:alpha=1:enable='between(t,316,914)',fade=out:st=915:d=1:alpha=1:enable='between(t,901,1499)',fade=in:st=1500:d=1:alpha=1:enable='between(t,916,1514)',fade=out:st=1515:d=1:alpha=1:enable='between(t,1501,2099)',fade=in:st=2100:d=1:alpha=1:enable='between(t,1516,2114)',fade=out:st=2115:d=1:alpha=1:enable='between(t,2101,2699)',fade=in:st=2700:d=1:alpha=1:enable='between(t,2116,2714)',fade=out:st=2715:d=1:alpha=1:enable='between(t,2701,3300)',hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"0:1",
"[v]",
true)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
true,
100,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
true,
100,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,scale=384:-1,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"0:1",
"[v]",
true)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
false,
90,
"[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"0:1",
"[v]",
false)]
[TestCase(
false,
false,
false,
WatermarkLocation.TopLeft,
false,
90,
"[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,colorchannelmixer=aa=0.90,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"0:1",
"[v]",
true)]
// TODO: do we need these anymore? interlaced content that isn't handled by mpeg2_cuvid?
// [TestCase(
// false,
// true,
// false,
// WatermarkLocation.TopLeft,
// false,
// 100,
// "[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
// "0:1",
// "[v]")]
// [TestCase(
// false,
// true,
// false,
// WatermarkLocation.TopLeft,
// true,
// 100,
// "[0:0]yadif=1[vt];[1:v]scale=384:-1[wmp];[vt][wmp]overlay=x=134:y=54[v]",
// "0:1",
// "[v]")]
// [TestCase(
// true,
// true,
// false,
// WatermarkLocation.TopLeft,
// false,
// 100,
// "[0:1]apad=whole_dur=3300000ms[a];[0:0]yadif=1[vt];[vt][1:v]overlay=x=134:y=54[v]",
// "[a]",
// "[v]")]
[TestCase(
true,
false,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=format=yuv420p[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54[v]",
"[a]",
"[v]",
false)]
[TestCase(
true,
false,
false,
WatermarkLocation.TopLeft,
false,
100,
"[0:1]apad=whole_dur=3300000ms[a];[0:0]scale_cuda=1920:1080,setsar=1,hwdownload,format=nv12,format=yuv420p,hwupload_cuda[vt];[1:v]format=yuva420p,hwupload_cuda[wmp];[vt][wmp]overlay_cuda=x=134:y=54,hwupload[v]",
"[a]",
"[v]",
true)]
public void Should_Return_NVENC_Watermark(
bool alignAudio,
bool deinterlace,
bool intermittent,
WatermarkLocation location,
bool scaled,
int opacity,
string expectedVideoFilter,
string expectedAudioLabel,
string expectedVideoLabel,
bool scaledSource)
{
var watermark = new ChannelWatermark
{
Mode = intermittent
? ChannelWatermarkMode.Intermittent
: ChannelWatermarkMode.Permanent,
DurationSeconds = intermittent ? 15 : 0,
FrequencyMinutes = intermittent ? 10 : 0,
Location = location,
Size = scaled ? WatermarkSize.Scaled : WatermarkSize.ActualSize,
WidthPercent = scaled ? 20 : 0,
Opacity = opacity,
HorizontalMarginPercent = 7,
VerticalMarginPercent = 5
};
Option<List<FadePoint>> maybeFadePoints = watermark.Mode == ChannelWatermarkMode.Intermittent
? Some(
WatermarkCalculator.CalculateFadePoints(
new DateTimeOffset(2022, 01, 31, 12, 25, 0, TimeSpan.FromHours(-5)),
TimeSpan.Zero,
TimeSpan.FromMinutes(55),
TimeSpan.Zero,
watermark.FrequencyMinutes,
watermark.DurationSeconds))
: None;
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc)
.WithWatermark(
Some(watermark),
maybeFadePoints,
new Resolution { Width = 1920, Height = 1080 },
None)
.WithDeinterlace(deinterlace)
.WithAlignedAudio(alignAudio ? Some(TimeSpan.FromMinutes(55)) : None);
if (scaledSource)
{
builder = builder.WithScaling(new Resolution { Width = 1920, Height = 1080 });
}
Option<FFmpegComplexFilter> result = builder.Build(false, 0, 0, 0, 1, false);
result.IsSome.Should().BeTrue();
result.IfSome(
filter =>
{
filter.ComplexFilter.Should().Be(expectedVideoFilter);
filter.AudioLabel.Should().Be(expectedAudioLabel);
filter.VideoLabel.Should().Be(expectedVideoLabel);
});
}
[Test] [Test]
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")] [TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]
@@ -4,6 +4,7 @@ using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.FFmpeg namespace ErsatzTV.Core.Tests.FFmpeg
{ {
@@ -16,10 +17,33 @@ namespace ErsatzTV.Core.Tests.FFmpeg
private readonly FFmpegPlaybackSettingsCalculator _calculator; private readonly FFmpegPlaybackSettingsCalculator _calculator;
public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator(); public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator();
[Test]
public void Should_Not_GenPts_ForHlsSegmenter()
{
FFmpegProfile ffmpegProfile = TestProfile();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreamingSegmenter,
ffmpegProfile,
new MediaVersion(),
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false,
None);
actual.FormatFlags.Should().NotContain("+genpts");
}
[Test] [Test]
public void Should_UseSpecifiedThreadCount_ForTransportStream() public void Should_Not_UseSpecifiedThreadCount_ForTransportStream()
{ {
// MPEG-TS requires realtime output which is hardcoded to a single thread
FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
@@ -31,18 +55,20 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ThreadCount.Should().Be(7); actual.ThreadCount.Should().Be(1);
} }
[Test] [Test]
public void Should_UseSpecifiedThreadCount_ForHttpLiveStreaming() public void Should_UseSpecifiedThreadCount_ForHttpLiveStreamingSegmenter()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreamingDirect, StreamingMode.HttpLiveStreamingSegmenter,
ffmpegProfile, ffmpegProfile,
new MediaVersion(), new MediaVersion(),
new MediaStream(), new MediaStream(),
@@ -50,7 +76,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ThreadCount.Should().Be(7); actual.ThreadCount.Should().Be(7);
} }
@@ -69,7 +97,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length); actual.FormatFlags.Count.Should().Be(expected.Length);
@@ -90,7 +120,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length); actual.FormatFlags.Count.Should().Be(expected.Length);
@@ -111,7 +143,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.RealtimeOutput.Should().BeTrue(); actual.RealtimeOutput.Should().BeTrue();
} }
@@ -130,7 +164,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.RealtimeOutput.Should().BeTrue(); actual.RealtimeOutput.Should().BeTrue();
} }
@@ -151,7 +187,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
now, now,
now.AddMinutes(5), now.AddMinutes(5),
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.StreamSeek.IsSome.Should().BeTrue(); actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@@ -173,7 +211,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
now, now,
now.AddMinutes(5), now.AddMinutes(5),
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.StreamSeek.IsSome.Should().BeTrue(); actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@@ -193,7 +233,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
} }
@@ -219,7 +261,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
} }
@@ -245,7 +289,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
} }
@@ -271,7 +317,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -299,7 +347,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -326,7 +376,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 }); IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
scaledSize.Width.Should().Be(1280); scaledSize.Width.Should().Be(1280);
@@ -356,7 +408,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -384,7 +438,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -413,7 +469,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -445,7 +503,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -477,7 +537,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -508,7 +570,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -540,7 +604,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -574,7 +640,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -606,7 +674,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -637,7 +707,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -667,7 +739,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -699,7 +773,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -727,7 +803,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioCodec.Should().Be("aac"); actual.AudioCodec.Should().Be("aac");
} }
@@ -752,7 +830,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
} }
@@ -778,7 +858,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioCodec.Should().Be("aac"); actual.AudioCodec.Should().Be("aac");
} }
@@ -804,7 +886,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
} }
@@ -831,7 +915,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioBitrate.IfNone(0).Should().Be(2424); actual.AudioBitrate.IfNone(0).Should().Be(2424);
} }
@@ -858,7 +944,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioBufferSize.IfNone(0).Should().Be(2424); actual.AudioBufferSize.IfNone(0).Should().Be(2424);
} }
@@ -885,7 +973,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioChannels.IfNone(0).Should().Be(6); actual.AudioChannels.IfNone(0).Should().Be(6);
} }
@@ -912,7 +1002,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioSampleRate.IfNone(0).Should().Be(48); actual.AudioSampleRate.IfNone(0).Should().Be(48);
} }
@@ -938,7 +1030,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioChannels.IfNone(0).Should().Be(6); actual.AudioChannels.IfNone(0).Should().Be(6);
} }
@@ -964,7 +1058,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.AudioSampleRate.IfNone(0).Should().Be(48); actual.AudioSampleRate.IfNone(0).Should().Be(48);
} }
@@ -991,7 +1087,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.FromMinutes(2)); TimeSpan.FromMinutes(2),
false,
None);
actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2)); actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2));
} }
@@ -1017,7 +1115,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.NormalizeLoudness.Should().BeTrue(); actual.NormalizeLoudness.Should().BeTrue();
} }
@@ -1043,7 +1143,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.NormalizeLoudness.Should().BeFalse(); actual.NormalizeLoudness.Should().BeFalse();
} }
@@ -1071,7 +1173,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
None);
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv); actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
} }
@@ -1,6 +1,9 @@
using System; using System;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using FluentAssertions; using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework; using NUnit.Framework;
namespace ErsatzTV.Core.Tests.FFmpeg namespace ErsatzTV.Core.Tests.FFmpeg
@@ -8,8 +11,19 @@ namespace ErsatzTV.Core.Tests.FFmpeg
[TestFixture] [TestFixture]
public class HlsPlaylistFilterTests public class HlsPlaylistFilterTests
{ {
private HlsPlaylistFilter _hlsPlaylistFilter;
[SetUp]
public void SetUp()
{
_hlsPlaylistFilter = new HlsPlaylistFilter(
new Mock<ITempFilePool>().Object,
new Mock<ILogger<HlsPlaylistFilter>>().Object
);
}
[Test] [Test]
public void HlsPlaylistFilter_ShouldRewriteProgramDateTime() public void _hlsPlaylistFilter_ShouldRewriteProgramDateTime()
{ {
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
string[] input = NormalizeLineEndings(@"#EXTM3U string[] input = NormalizeLineEndings(@"#EXTM3U
@@ -28,7 +42,7 @@ live001138.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
live001139.ts").Split(Environment.NewLine); live001139.ts").Split(Environment.NewLine);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input);
result.PlaylistStart.Should().Be(start); result.PlaylistStart.Should().Be(start);
result.Sequence.Should().Be(1137); result.Sequence.Should().Be(1137);
@@ -53,7 +67,7 @@ live001139.ts
} }
[Test] [Test]
public void HlsPlaylistFilter_ShouldLimitSegments() public void _hlsPlaylistFilter_ShouldLimitSegments()
{ {
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
string[] input = NormalizeLineEndings(@"#EXTM3U string[] input = NormalizeLineEndings(@"#EXTM3U
@@ -72,7 +86,7 @@ live001138.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
live001139.ts").Split(Environment.NewLine); live001139.ts").Split(Environment.NewLine);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2);
result.PlaylistStart.Should().Be(start); result.PlaylistStart.Should().Be(start);
result.Sequence.Should().Be(1137); result.Sequence.Should().Be(1137);
@@ -94,7 +108,7 @@ live001138.ts
} }
[Test] [Test]
public void HlsPlaylistFilter_ShouldAddDiscontinuity() public void _hlsPlaylistFilter_ShouldAddDiscontinuity()
{ {
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
string[] input = NormalizeLineEndings(@"#EXTM3U string[] input = NormalizeLineEndings(@"#EXTM3U
@@ -113,7 +127,7 @@ live001138.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
live001139.ts").Split(Environment.NewLine); live001139.ts").Split(Environment.NewLine);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist( TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(
start, start,
start.AddSeconds(-30), start.AddSeconds(-30),
input, input,
@@ -144,7 +158,7 @@ live001139.ts
} }
[Test] [Test]
public void HlsPlaylistFilter_ShouldFilterOldSegments() public void _hlsPlaylistFilter_ShouldFilterOldSegments()
{ {
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
string[] input = NormalizeLineEndings(@"#EXTM3U string[] input = NormalizeLineEndings(@"#EXTM3U
@@ -163,7 +177,7 @@ live001138.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
live001139.ts").Split(Environment.NewLine); live001139.ts").Split(Environment.NewLine);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
result.PlaylistStart.Should().Be(start.AddSeconds(8)); result.PlaylistStart.Should().Be(start.AddSeconds(8));
result.Sequence.Should().Be(1139); result.Sequence.Should().Be(1139);
@@ -182,7 +196,7 @@ live001139.ts
} }
[Test] [Test]
public void HlsPlaylistFilter_ShouldFilterOldDiscontinuity() public void _hlsPlaylistFilter_ShouldFilterOldDiscontinuity()
{ {
var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5)); var start = new DateTimeOffset(2021, 10, 9, 8, 0, 0, TimeSpan.FromHours(-5));
string[] input = NormalizeLineEndings(@"#EXTM3U string[] input = NormalizeLineEndings(@"#EXTM3U
@@ -202,7 +216,7 @@ live001138.ts
#EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500 #EXT-X-PROGRAM-DATE-TIME:2021-10-08T08:34:57.320-0500
live001139.ts").Split(Environment.NewLine); live001139.ts").Split(Environment.NewLine);
TrimPlaylistResult result = HlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input); TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
result.PlaylistStart.Should().Be(start.AddSeconds(8)); result.PlaylistStart.Should().Be(start.AddSeconds(8));
result.Sequence.Should().Be(1139); result.Sequence.Should().Be(1139);
+146 -39
View File
@@ -4,6 +4,8 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
@@ -37,23 +39,53 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Assert.Pass(); Assert.Pass();
} }
public record InputFormat(string Encoder, string PixelFormat);
public enum Padding
{
NoPadding,
WithPadding
}
private class TestData private class TestData
{ {
public static string[] InputCodecs = public static Padding[] Paddings =
{ {
"h264", Padding.NoPadding,
"mpeg2video", Padding.WithPadding
"hevc",
"mpeg4"
}; };
public static string[] InputPixelFormats = public static VideoScanKind[] VideoScanKinds =
{ {
"yuv420p", VideoScanKind.Progressive,
"yuv420p10le", VideoScanKind.Interlaced
"yuvj420p", };
"yuv444p",
"yuv444p10le" public static InputFormat[] InputFormats =
{
new("libx264", "yuv420p"),
new("libx264", "yuvj420p"),
new("libx264", "yuv420p10le"),
// new("libx264", "yuv444p10le"),
new("mpeg1video", "yuv420p"),
new("mpeg2video", "yuv420p"),
new("libx265", "yuv420p"),
new("libx265", "yuv420p10le"),
new("mpeg4", "yuv420p"),
new("libvpx-vp9", "yuv420p"),
// new("libaom-av1", "yuv420p")
// av1 yuv420p10le 51
new("msmpeg4v2", "yuv420p"),
new("msmpeg4v3", "yuv420p")
// wmv3 yuv420p 1
}; };
public static Resolution[] Resolutions = public static Resolution[] Resolutions =
@@ -105,37 +137,62 @@ namespace ErsatzTV.Core.Tests.FFmpeg
{ {
HardwareAccelerationKind.VideoToolbox HardwareAccelerationKind.VideoToolbox
}; };
public static string[] QsvCodecs =
{
"h264_qsv",
"hevc_qsv"
};
public static HardwareAccelerationKind[] QsvAcceleration =
{
HardwareAccelerationKind.Qsv
};
} }
[Test, Combinatorial] [Test, Combinatorial]
public async Task Transcode( public async Task Transcode(
[ValueSource(typeof(TestData), nameof(TestData.InputCodecs))] [ValueSource(typeof(TestData), nameof(TestData.InputFormats))]
string inputCodec, InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.InputPixelFormats))]
string inputPixelFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))] [ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution, Resolution profileResolution,
[Values(true, false)] [ValueSource(typeof(TestData), nameof(TestData.Paddings))]
bool pad, Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
VideoScanKind videoScanKind,
// [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec, [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration) [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
[ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec, // [ValueSource(typeof(TestData), nameof(TestData.QsvCodecs))] string profileCodec,
[ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration) // [ValueSource(typeof(TestData), nameof(TestData.QsvAcceleration))] HardwareAccelerationKind profileAcceleration)
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
{ {
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
{
if (videoScanKind == VideoScanKind.Interlaced)
{
Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content");
return;
}
}
string name = GetStringSha256Hash( string name = GetStringSha256Hash(
$"{inputCodec}_{inputPixelFormat}_{pad}_{profileResolution}_{profileCodec}_{profileAcceleration}"); $"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}");
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file)) if (!File.Exists(file))
{ {
string resolution = pad ? "1920x1060" : "1920x1080"; string resolution = padding == Padding.WithPadding ? "1920x1060" : "1920x1080";
string videoFilter = videoScanKind == VideoScanKind.Interlaced ? "-vf tinterlace=interleave_top,fieldorder=tff" : string.Empty;
string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty;
var args = string args =
$"-y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {file}"; $"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}";
var p1 = new Process var p1 = new Process
{ {
StartInfo = new ProcessStartInfo StartInfo = new ProcessStartInfo
@@ -152,19 +209,35 @@ namespace ErsatzTV.Core.Tests.FFmpeg
p1.ExitCode.Should().Be(0); p1.ExitCode.Should().Be(0);
} }
var service = new FFmpegProcessService( var oldService = new FFmpegProcessService(
new FFmpegPlaybackSettingsCalculator(), new FFmpegPlaybackSettingsCalculator(),
new FakeStreamSelector(), new FakeStreamSelector(),
new Mock<IImageCache>().Object, new Mock<IImageCache>().Object,
new Mock<ITempFilePool>().Object, new Mock<ITempFilePool>().Object,
new Mock<ILogger<FFmpegProcessService>>().Object); new Mock<ILogger<FFmpegProcessService>>().Object);
MediaVersion v = new MediaVersion(); var service = new FFmpegLibraryProcessService(
oldService,
new FFmpegPlaybackSettingsCalculator(),
new FakeStreamSelector(),
new Mock<ILogger<FFmpegLibraryProcessService>>().Object);
var v = new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = file }
}
};
var metadataRepository = new Mock<IMetadataRepository>(); var metadataRepository = new Mock<IMetadataRepository>();
metadataRepository metadataRepository
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>())) .Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
.Callback<MediaItem, MediaVersion, bool>((_, version, _) => v = version); .Callback<MediaItem, MediaVersion, bool>((_, version, _) =>
{
version.MediaFiles = v.MediaFiles;
v = version;
});
var localStatisticsProvider = new LocalStatisticsProvider( var localStatisticsProvider = new LocalStatisticsProvider(
metadataRepository.Object, metadataRepository.Object,
@@ -194,10 +267,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
new Channel(Guid.NewGuid()) new Channel(Guid.NewGuid())
{ {
Number = "1",
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with FFmpegProfile = FFmpegProfile.New("test", profileResolution) with
{ {
HardwareAcceleration = profileAcceleration, HardwareAcceleration = profileAcceleration,
VideoCodec = profileCodec VideoCodec = profileCodec,
AudioCodec = "aac"
}, },
StreamingMode = StreamingMode.TransportStream StreamingMode = StreamingMode.TransportStream
}, },
@@ -214,28 +289,56 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
FillerKind.None, FillerKind.None,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.FromSeconds(5)); TimeSpan.FromSeconds(5),
0,
None);
process.StartInfo.RedirectStandardError = true; process.StartInfo.RedirectStandardError = true;
process.EnableRaisingEvents = true;
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}"); // Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
process.Start().Should().BeTrue(); process.Start().Should().BeTrue();
process.BeginOutputReadLine();
string error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
// ReSharper disable once MethodHasAsyncOverload
process.WaitForExit();
string[] unsupportedMessages = string[] unsupportedMessages =
{ {
"No support for codec", "No support for codec",
"No usable", "No usable",
"Provided device doesn't support" "Provided device doesn't support"
}; };
var errorBuffer = new StringBuilder();
if (profileAcceleration != HardwareAccelerationKind.None && unsupportedMessages.Any(error.Contains)) process.ErrorDataReceived += (_, errorLine) =>
{
string data = errorLine.Data ?? string.Empty;
errorBuffer.AppendLine(data);
};
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// string error = await process.StandardError.ReadToEndAsync();
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
await process.WaitForExitAsync(timeoutSignal.Token);
// ReSharper disable once MethodHasAsyncOverload
process.WaitForExit();
}
catch (OperationCanceledException)
{
process.Kill();
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}");
return;
}
var error = errorBuffer.ToString();
bool isUnsupported = unsupportedMessages.Any(error.Contains);
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported)
{ {
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList(); var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}"); process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}");
@@ -248,8 +351,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
} }
else else
{ {
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'"); var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
process.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs)); process.ExitCode.Should().Be(0, errorBuffer + Environment.NewLine + string.Join(" ", quotedArgs));
if (process.ExitCode == 0)
{
Console.WriteLine(string.Join(" ", quotedArgs));
}
} }
} }
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Core.FFmpeg;
using FluentAssertions;
using NUnit.Framework;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Tests.FFmpeg;
[TestFixture]
public class WatermarkCalculatorTests
{
[Test]
public void EntireVideoBetweenWatermarks_ShouldReturn_EmptyFadePointList()
{
List<FadePoint> actual = WatermarkCalculator.CalculateFadePoints(
new DateTimeOffset(2022, 01, 31, 13, 34, 00, TimeSpan.FromHours(-5)),
TimeSpan.Zero,
TimeSpan.FromMinutes(5),
None,
15,
10);
actual.Should().HaveCount(0);
}
}
@@ -37,5 +37,6 @@ namespace ErsatzTV.Core.Tests.Fakes
throw new NotSupportedException(); throw new NotSupportedException();
public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask(); public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask();
public Task<Option<string>> GetNameFromKey(CollectionKey emptyCollection) => Option<string>.None.AsTask();
} }
} }
@@ -81,24 +81,6 @@ namespace ErsatzTV.Core.Tests.Metadata
private Mock<ILocalMetadataProvider> _localMetadataProvider; private Mock<ILocalMetadataProvider> _localMetadataProvider;
private Mock<IImageCache> _imageCache; private Mock<IImageCache> _imageCache;
[Test]
public async Task Missing_Folder()
{
MovieFolderScanner service = GetService(
new FakeFileEntry(Path.Combine(FakeRoot, Path.Combine("Movie (2020)", "Movie (2020).mkv")))
);
var libraryPath = new LibraryPath { Path = BadFakeRoot, LibraryFolders = new List<LibraryFolder>() };
Either<BaseError, Unit> result = await service.ScanFolder(
libraryPath,
FFprobePath,
0,
1);
result.IsLeft.Should().BeTrue();
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
}
[Test] [Test]
public async Task NewMovie_Statistics_And_FallbackMetadata( public async Task NewMovie_Statistics_And_FallbackMetadata(
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
@@ -624,8 +624,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
Anchor = new PlayoutAnchor Anchor = new PlayoutAnchor
{ {
NextStart = HoursAfterMidnight(9).UtcDateTime, NextStart = HoursAfterMidnight(9).UtcDateTime,
NextScheduleItem = items[0], ScheduleItemsEnumeratorState = new CollectionEnumeratorState
NextScheduleItemId = 1, {
Index = 0,
Seed = 1
},
InFlood = true InFlood = true
} }
}; };
@@ -918,8 +921,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
Anchor = new PlayoutAnchor Anchor = new PlayoutAnchor
{ {
NextStart = HoursAfterMidnight(1).UtcDateTime, NextStart = HoursAfterMidnight(1).UtcDateTime,
NextScheduleItem = items[0], ScheduleItemsEnumeratorState = new CollectionEnumeratorState
NextScheduleItemId = 1, {
Index = 0,
Seed = 1
},
MultipleRemaining = 2 MultipleRemaining = 2
} }
}; };
@@ -951,7 +957,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
result.Items[3].MediaItemId.Should().Be(2); result.Items[3].MediaItemId.Should().Be(2);
result.Anchor.NextScheduleItem.Should().Be(items[1]); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
result.Anchor.MultipleRemaining.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1);
} }
@@ -1048,7 +1054,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
result.Items[4].MediaItemId.Should().Be(5); result.Items[4].MediaItemId.Should().Be(5);
result.Anchor.NextScheduleItem.Should().Be(items[0]); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
result.Anchor.MultipleRemaining.Should().BeNull(); result.Anchor.MultipleRemaining.Should().BeNull();
} }
@@ -1116,8 +1122,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
Anchor = new PlayoutAnchor Anchor = new PlayoutAnchor
{ {
NextStart = HoursAfterMidnight(1).UtcDateTime, NextStart = HoursAfterMidnight(1).UtcDateTime,
NextScheduleItem = items[0], ScheduleItemsEnumeratorState = new CollectionEnumeratorState
NextScheduleItemId = 1, {
Index = 0,
Seed = 1
},
DurationFinish = HoursAfterMidnight(3).UtcDateTime DurationFinish = HoursAfterMidnight(3).UtcDateTime
} }
}; };
@@ -1149,7 +1158,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4));
result.Items[3].MediaItemId.Should().Be(2); result.Items[3].MediaItemId.Should().Be(2);
result.Anchor.NextScheduleItem.Should().Be(items[1]); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1);
result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime);
} }
@@ -1284,7 +1293,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0)); result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0));
result.Items[11].MediaItemId.Should().Be(3); result.Items[11].MediaItemId.Should().Be(3);
result.Anchor.NextScheduleItem.Should().Be(items[0]); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
result.Anchor.DurationFinish.Should().BeNull(); result.Anchor.DurationFinish.Should().BeNull();
} }
@@ -1361,7 +1370,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5));
result.Items[5].MediaItemId.Should().Be(4); result.Items[5].MediaItemId.Should().Be(4);
result.Anchor.NextScheduleItem.Should().Be(items[0]); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0);
result.Anchor.DurationFinish.Should().BeNull(); result.Anchor.DurationFinish.Should().BeNull();
} }
@@ -31,19 +31,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
PlaybackOrder = PlaybackOrder.Chronological PlaybackOrder = PlaybackOrder.Chronological
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -51,26 +57,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
@@ -93,20 +99,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
PlaybackOrder = PlaybackOrder.Chronological, PlaybackOrder = PlaybackOrder.Chronological,
CustomTitle = "Custom Title" CustomTitle = "Custom Title"
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -114,28 +126,28 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[0].CustomTitle.Should().Be("Custom Title"); playoutItems[0].CustomTitle.Should().Be("Custom Title");
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].CustomTitle.Should().Be("Custom Title"); playoutItems[1].CustomTitle.Should().Be("Custom Title");
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(1); playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
@@ -158,20 +170,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
TailMode = TailMode.None, TailMode = TailMode.None,
PlaybackOrder = PlaybackOrder.Chronological PlaybackOrder = PlaybackOrder.Chronological
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -179,26 +197,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
@@ -220,48 +238,54 @@ namespace ErsatzTV.Core.Tests.Scheduling
TailMode = TailMode.Offline, TailMode = TailMode.Offline,
PlaybackOrder = PlaybackOrder.Chronological PlaybackOrder = PlaybackOrder.Chronological
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
// duration block should end after exact duration, with gap // duration block should end after exact duration, with gap
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
@@ -290,6 +314,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionTwo.Id CollectionId = collectionTwo.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -299,15 +327,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionTwo.MediaItems, collectionTwo.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -315,7 +345,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -323,25 +353,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
@@ -370,6 +400,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionTwo.Id CollectionId = collectionTwo.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -379,15 +413,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionTwo.MediaItems, collectionTwo.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -395,7 +431,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -403,37 +439,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
@@ -462,6 +498,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionTwo.Id CollectionId = collectionTwo.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -471,23 +511,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionTwo.MediaItems, collectionTwo.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutItems.Last().FinishOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
playoutBuilderState.DurationFinish.IsNone.Should().BeTrue(); playoutBuilderState.DurationFinish.IsNone.Should().BeTrue();
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -495,37 +537,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].GuideFinish.HasValue.Should().BeFalse(); playoutItems[4].GuideFinish.HasValue.Should().BeFalse();
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].GuideFinish.HasValue.Should().BeFalse(); playoutItems[5].GuideFinish.HasValue.Should().BeFalse();
@@ -561,6 +603,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionThree.Id CollectionId = collectionThree.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -574,9 +620,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionThree.MediaItems, collectionThree.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerDuration(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -586,9 +634,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -596,7 +644,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -605,43 +653,43 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(7); playoutItems.Count.Should().Be(7);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[0].GuideFinish.HasValue.Should().BeFalse(); playoutItems[0].GuideFinish.HasValue.Should().BeFalse();
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].GuideFinish.HasValue.Should().BeFalse(); playoutItems[1].GuideFinish.HasValue.Should().BeFalse();
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].GuideFinish.HasValue.Should().BeTrue(); playoutItems[2].GuideFinish.HasValue.Should().BeTrue();
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].GuideFinish.HasValue.Should().BeFalse(); playoutItems[3].GuideFinish.HasValue.Should().BeFalse();
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].GuideFinish.HasValue.Should().BeFalse(); playoutItems[4].GuideFinish.HasValue.Should().BeFalse();
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].GuideFinish.HasValue.Should().BeFalse(); playoutItems[5].GuideFinish.HasValue.Should().BeFalse();
playoutItems[6].MediaItemId.Should().Be(5); playoutItems[6].MediaItemId.Should().Be(5);
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[6].GuideGroup.Should().Be(3); playoutItems[6].GuideGroup.Should().Be(3);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
playoutItems[6].GuideFinish.HasValue.Should().BeFalse(); playoutItems[6].GuideFinish.HasValue.Should().BeFalse();
@@ -40,16 +40,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -57,29 +63,29 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
[Test] [Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood() public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood()
{ {
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
@@ -109,16 +115,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
StartTime = TimeSpan.FromHours(3), StartTime = TimeSpan.FromHours(3),
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -126,24 +138,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
@@ -188,15 +200,21 @@ namespace ErsatzTV.Core.Tests.Scheduling
NextScheduleItem NextScheduleItem
}; };
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -204,7 +222,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -212,32 +230,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[2].MediaItemId.Should().Be(2); playoutItems[2].MediaItemId.Should().Be(2);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[2].GuideGroup.Should().Be(2); playoutItems[2].GuideGroup.Should().Be(2);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(4); playoutItems[3].MediaItemId.Should().Be(4);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 55, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(2); playoutItems[3].GuideGroup.Should().Be(2);
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[4].MediaItemId.Should().Be(1); playoutItems[4].MediaItemId.Should().Be(1);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.None); playoutItems[4].FillerKind.Should().Be(FillerKind.None);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[5].FillerKind.Should().Be(FillerKind.PostRoll);
} }
@@ -268,16 +286,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -285,24 +309,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
@@ -343,16 +367,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -360,7 +390,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -368,32 +398,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
} }
@@ -434,16 +464,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -451,7 +487,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -459,22 +495,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
} }
@@ -515,16 +551,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -532,7 +574,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -540,32 +582,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
} }
@@ -616,10 +658,16 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -629,9 +677,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -639,7 +687,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -648,37 +696,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(7); playoutItems.Count.Should().Be(7);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[6].MediaItemId.Should().Be(5); playoutItems[6].MediaItemId.Should().Be(5);
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[6].GuideGroup.Should().Be(3); playoutItems[6].GuideGroup.Should().Be(3);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
} }
@@ -729,10 +777,16 @@ namespace ErsatzTV.Core.Tests.Scheduling
scheduleItem, scheduleItem,
NextScheduleItem NextScheduleItem
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
sortedScheduleItems,
new CollectionEnumeratorState());
var scheduler = new PlayoutModeSchedulerFlood(sortedScheduleItems, new Mock<ILogger>().Object); PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerFlood(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -742,9 +796,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -752,7 +806,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(1);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0); enumerator2.State.Index.Should().Be(0);
@@ -761,17 +815,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
@@ -33,6 +33,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
FallbackFiller = null, FallbackFiller = null,
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -42,16 +46,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ {
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -59,24 +65,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
@@ -98,6 +104,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
FallbackFiller = null, FallbackFiller = null,
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -107,16 +117,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ {
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems } { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -124,24 +136,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
@@ -169,6 +181,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
FallbackFiller = null, FallbackFiller = null,
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -183,16 +199,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems } { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -200,7 +218,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -208,32 +226,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
} }
@@ -261,6 +279,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
}, },
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -275,16 +297,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems } { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionTwo.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -292,7 +316,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -300,22 +324,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[3].FillerKind.Should().Be(FillerKind.Fallback);
} }
@@ -343,6 +367,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
FallbackFiller = null, FallbackFiller = null,
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -357,16 +385,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems }, { CollectionKey.ForScheduleItem(scheduleItem), collectionOne.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems } { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -374,7 +404,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -382,32 +412,32 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(6); playoutItems.Count.Should().Be(6);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
} }
@@ -441,6 +471,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
}, },
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -461,9 +495,11 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems } { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -473,9 +509,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -483,7 +519,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -492,37 +528,37 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(7); playoutItems.Count.Should().Be(7);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddMinutes(55)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddMinutes(55));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(1, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(1, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[3].GuideGroup.Should().Be(3); playoutItems[3].GuideGroup.Should().Be(3);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(4); playoutItems[4].MediaItemId.Should().Be(4);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[4].GuideGroup.Should().Be(3); playoutItems[4].GuideGroup.Should().Be(3);
playoutItems[4].FillerKind.Should().Be(FillerKind.Tail); playoutItems[4].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[5].MediaItemId.Should().Be(3); playoutItems[5].MediaItemId.Should().Be(3);
playoutItems[5].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[5].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[5].GuideGroup.Should().Be(3); playoutItems[5].GuideGroup.Should().Be(3);
playoutItems[5].FillerKind.Should().Be(FillerKind.Tail); playoutItems[5].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[6].MediaItemId.Should().Be(5); playoutItems[6].MediaItemId.Should().Be(5);
playoutItems[6].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutItems[6].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[6].GuideGroup.Should().Be(3); playoutItems[6].GuideGroup.Should().Be(3);
playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[6].FillerKind.Should().Be(FillerKind.Fallback);
} }
@@ -556,6 +592,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
}, },
Count = 3 Count = 3
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -575,10 +615,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems }, { CollectionKey.ForFillerPreset(scheduleItem.TailFiller), collectionTwo.MediaItems },
{ CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems } { CollectionKey.ForFillerPreset(scheduleItem.FallbackFiller), collectionThree.MediaItems }
}.ToMap(); }.ToMap();
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerMultiple(collectionMediaItems, new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -588,9 +630,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(4); playoutBuilderState.NextGuideGroup.Should().Be(4);
@@ -598,7 +640,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0); enumerator2.State.Index.Should().Be(0);
@@ -607,17 +649,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(3); playoutItems.Count.Should().Be(3);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(2); playoutItems[1].MediaItemId.Should().Be(2);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(2); playoutItems[1].GuideGroup.Should().Be(2);
playoutItems[1].FillerKind.Should().Be(FillerKind.None); playoutItems[1].FillerKind.Should().Be(FillerKind.None);
playoutItems[2].MediaItemId.Should().Be(1); playoutItems[2].MediaItemId.Should().Be(1);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.AddHours(2)); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.AddHours(2));
playoutItems[2].GuideGroup.Should().Be(3); playoutItems[2].GuideGroup.Should().Be(3);
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
@@ -30,20 +30,26 @@ namespace ErsatzTV.Core.Tests.Scheduling
TailFiller = null, TailFiller = null,
FallbackFiller = null FallbackFiller = null
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator = new ChronologicalMediaCollectionEnumerator( var enumerator = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator), CollectionEnumerators(scheduleItem, enumerator),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -51,14 +57,14 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator.State.Index.Should().Be(1); enumerator.State.Index.Should().Be(1);
playoutItems.Count.Should().Be(1); playoutItems.Count.Should().Be(1);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
} }
@@ -91,6 +97,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
Collection = collectionThree Collection = collectionThree
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -104,15 +114,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionThree.MediaItems, collectionThree.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(1)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -120,7 +132,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0); enumerator2.State.Index.Should().Be(0);
@@ -129,7 +141,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(1); playoutItems.Count.Should().Be(1);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
} }
@@ -156,6 +168,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
}, },
FallbackFiller = null FallbackFiller = null
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -164,16 +180,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
var enumerator2 = new ChronologicalMediaCollectionEnumerator( var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems, collectionTwo.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -181,7 +199,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -189,22 +207,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[2].MediaItemId.Should().Be(4); playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1); playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(1); playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
} }
@@ -231,6 +249,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionTwo.Id CollectionId = collectionTwo.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -239,16 +261,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
var enumerator2 = new ChronologicalMediaCollectionEnumerator( var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems, collectionTwo.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.FallbackFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -256,7 +280,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -264,12 +288,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(2); playoutItems.Count.Should().Be(2);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.AddHours(1)); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.AddHours(1));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[1].FillerKind.Should().Be(FillerKind.Fallback);
} }
@@ -296,6 +320,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
}, },
FallbackFiller = null FallbackFiller = null
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -304,16 +332,18 @@ namespace ErsatzTV.Core.Tests.Scheduling
var enumerator2 = new ChronologicalMediaCollectionEnumerator( var enumerator2 = new ChronologicalMediaCollectionEnumerator(
collectionTwo.MediaItems, collectionTwo.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.TailFiller, enumerator2),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -321,7 +351,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -329,22 +359,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[2].MediaItemId.Should().Be(4); playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[2].GuideGroup.Should().Be(1); playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[3].GuideGroup.Should().Be(1); playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
} }
@@ -377,6 +407,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionThree.Id CollectionId = collectionThree.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -389,10 +423,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
var enumerator3 = new ChronologicalMediaCollectionEnumerator( var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems, collectionThree.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -402,9 +438,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -412,7 +448,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -421,27 +457,27 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(5); playoutItems.Count.Should().Be(5);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.Tail); playoutItems[1].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[2].MediaItemId.Should().Be(4); playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 49, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 49, 0)));
playoutItems[2].GuideGroup.Should().Be(1); playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.Tail); playoutItems[2].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 53, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 53, 0)));
playoutItems[3].GuideGroup.Should().Be(1); playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.Tail); playoutItems[3].FillerKind.Should().Be(FillerKind.Tail);
playoutItems[4].MediaItemId.Should().Be(5); playoutItems[4].MediaItemId.Should().Be(5);
playoutItems[4].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 57, 0))); playoutItems[4].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 57, 0)));
playoutItems[4].GuideGroup.Should().Be(1); playoutItems[4].GuideGroup.Should().Be(1);
playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback); playoutItems[4].FillerKind.Should().Be(FillerKind.Fallback);
} }
@@ -474,6 +510,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionThree.Id CollectionId = collectionThree.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -486,10 +526,12 @@ namespace ErsatzTV.Core.Tests.Scheduling
var enumerator3 = new ChronologicalMediaCollectionEnumerator( var enumerator3 = new ChronologicalMediaCollectionEnumerator(
collectionThree.MediaItems, collectionThree.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators( CollectionEnumerators(
scheduleItem, scheduleItem,
enumerator1, enumerator1,
@@ -499,9 +541,9 @@ namespace ErsatzTV.Core.Tests.Scheduling
enumerator3), enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -509,7 +551,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(0); enumerator2.State.Index.Should().Be(0);
@@ -518,7 +560,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(1); playoutItems.Count.Should().Be(1);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
} }
@@ -553,6 +595,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionThree.Id CollectionId = collectionThree.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -566,15 +612,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionThree.MediaItems, collectionThree.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -582,7 +630,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -590,22 +638,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[2].MediaItemId.Should().Be(4); playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1); playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(1); playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
} }
@@ -640,6 +688,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
CollectionId = collectionThree.Id CollectionId = collectionThree.Id
} }
}; };
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
new List<ProgramScheduleItem> { scheduleItem },
new CollectionEnumeratorState());
var enumerator1 = new ChronologicalMediaCollectionEnumerator( var enumerator1 = new ChronologicalMediaCollectionEnumerator(
collectionOne.MediaItems, collectionOne.MediaItems,
@@ -653,15 +705,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
collectionThree.MediaItems, collectionThree.MediaItems,
new CollectionEnumeratorState()); new CollectionEnumeratorState());
PlayoutBuilderState startState = StartState(scheduleItemsEnumerator);
var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object); var scheduler = new PlayoutModeSchedulerOne(new Mock<ILogger>().Object);
(PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule( (PlayoutBuilderState playoutBuilderState, List<PlayoutItem> playoutItems) = scheduler.Schedule(
StartState, startState,
CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3), CollectionEnumerators(scheduleItem, enumerator1, scheduleItem.PostRollFiller, enumerator2, scheduleItem.FallbackFiller, enumerator3),
scheduleItem, scheduleItem,
NextScheduleItem, NextScheduleItem,
HardStop); HardStop(scheduleItemsEnumerator));
playoutBuilderState.CurrentTime.Should().Be(StartState.CurrentTime.AddHours(3)); playoutBuilderState.CurrentTime.Should().Be(startState.CurrentTime.AddHours(3));
playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime); playoutItems.Last().FinishOffset.Should().Be(playoutBuilderState.CurrentTime);
playoutBuilderState.NextGuideGroup.Should().Be(2); playoutBuilderState.NextGuideGroup.Should().Be(2);
@@ -669,7 +723,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutBuilderState.InFlood.Should().BeFalse(); playoutBuilderState.InFlood.Should().BeFalse();
playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue(); playoutBuilderState.MultipleRemaining.IsNone.Should().BeTrue();
playoutBuilderState.InDurationFiller.Should().BeFalse(); playoutBuilderState.InDurationFiller.Should().BeFalse();
playoutBuilderState.ScheduleItemIndex.Should().Be(1); playoutBuilderState.ScheduleItemsEnumerator.State.Index.Should().Be(0);
enumerator1.State.Index.Should().Be(1); enumerator1.State.Index.Should().Be(1);
enumerator2.State.Index.Should().Be(1); enumerator2.State.Index.Should().Be(1);
@@ -678,22 +732,22 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems.Count.Should().Be(4); playoutItems.Count.Should().Be(4);
playoutItems[0].MediaItemId.Should().Be(1); playoutItems[0].MediaItemId.Should().Be(1);
playoutItems[0].StartOffset.Should().Be(StartState.CurrentTime); playoutItems[0].StartOffset.Should().Be(startState.CurrentTime);
playoutItems[0].GuideGroup.Should().Be(1); playoutItems[0].GuideGroup.Should().Be(1);
playoutItems[0].FillerKind.Should().Be(FillerKind.None); playoutItems[0].FillerKind.Should().Be(FillerKind.None);
playoutItems[1].MediaItemId.Should().Be(3); playoutItems[1].MediaItemId.Should().Be(3);
playoutItems[1].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 45, 0))); playoutItems[1].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 45, 0)));
playoutItems[1].GuideGroup.Should().Be(1); playoutItems[1].GuideGroup.Should().Be(1);
playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[1].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[2].MediaItemId.Should().Be(4); playoutItems[2].MediaItemId.Should().Be(4);
playoutItems[2].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 50, 0))); playoutItems[2].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 50, 0)));
playoutItems[2].GuideGroup.Should().Be(1); playoutItems[2].GuideGroup.Should().Be(1);
playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[2].FillerKind.Should().Be(FillerKind.PostRoll);
playoutItems[3].MediaItemId.Should().Be(3); playoutItems[3].MediaItemId.Should().Be(3);
playoutItems[3].StartOffset.Should().Be(StartState.CurrentTime.Add(new TimeSpan(2, 55, 0))); playoutItems[3].StartOffset.Should().Be(startState.CurrentTime.Add(new TimeSpan(2, 55, 0)));
playoutItems[3].GuideGroup.Should().Be(1); playoutItems[3].GuideGroup.Should().Be(1);
playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll); playoutItems[3].FillerKind.Should().Be(FillerKind.PostRoll);
} }
@@ -10,8 +10,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
{ {
public abstract class SchedulerTestBase public abstract class SchedulerTestBase
{ {
protected static PlayoutBuilderState StartState => new( protected static PlayoutBuilderState StartState(IScheduleItemsEnumerator scheduleItemsEnumerator) => new(
0, scheduleItemsEnumerator,
Prelude.None, Prelude.None,
Prelude.None, Prelude.None,
false, false,
@@ -24,7 +24,8 @@ namespace ErsatzTV.Core.Tests.Scheduling
StartTime = null StartTime = null
}; };
protected static DateTimeOffset HardStop => StartState.CurrentTime.AddHours(6); protected static DateTimeOffset HardStop(IScheduleItemsEnumerator scheduleItemsEnumerator) =>
StartState(scheduleItemsEnumerator).CurrentTime.AddHours(6);
protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators( protected static Dictionary<CollectionKey, IMediaCollectionEnumerator> CollectionEnumerators(
ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator) => ProgramScheduleItem scheduleItem, IMediaCollectionEnumerator enumerator) =>
+2
View File
@@ -13,6 +13,8 @@ namespace ErsatzTV.Core.Domain
public Guid UniqueId { get; init; } public Guid UniqueId { get; init; }
public string Number { get; set; } public string Number { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Group { get; set; }
public string Categories { get; set; }
public int FFmpegProfileId { get; set; } public int FFmpegProfileId { get; set; }
public FFmpegProfile FFmpegProfile { get; set; } public FFmpegProfile FFmpegProfile { get; set; }
public int? WatermarkId { get; set; } public int? WatermarkId { get; set; }
+5 -21
View File
@@ -1,4 +1,6 @@
namespace ErsatzTV.Core.Domain using ErsatzTV.FFmpeg.State;
namespace ErsatzTV.Core.Domain
{ {
public class ChannelWatermark public class ChannelWatermark
{ {
@@ -7,8 +9,8 @@
public ChannelWatermarkMode Mode { get; set; } public ChannelWatermarkMode Mode { get; set; }
public ChannelWatermarkImageSource ImageSource { get; set; } public ChannelWatermarkImageSource ImageSource { get; set; }
public string Image { get; set; } public string Image { get; set; }
public ChannelWatermarkLocation Location { get; set; } public WatermarkLocation Location { get; set; }
public ChannelWatermarkSize Size { get; set; } public WatermarkSize Size { get; set; }
public int WidthPercent { get; set; } public int WidthPercent { get; set; }
public int HorizontalMarginPercent { get; set; } public int HorizontalMarginPercent { get; set; }
public int VerticalMarginPercent { get; set; } public int VerticalMarginPercent { get; set; }
@@ -17,24 +19,6 @@
public int Opacity { get; set; } public int Opacity { get; set; }
} }
public enum ChannelWatermarkLocation
{
BottomRight = 0,
BottomLeft = 1,
TopRight = 2,
TopLeft = 3,
TopMiddle = 4,
RightMiddle = 5,
BottomMiddle = 6,
LeftMiddle = 7
}
public enum ChannelWatermarkSize
{
Scaled = 0,
ActualSize = 1
}
public enum ChannelWatermarkMode public enum ChannelWatermarkMode
{ {
None = 0, None = 0,
+2
View File
@@ -16,6 +16,8 @@
public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id"); public static ConfigElementKey FFmpegGlobalFallbackFillerId => new("ffmpeg.global_fallback_filler_id");
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds"); public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit"); public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
public static ConfigElementKey FFmpegInitialSegmentCount => new("ffmpeg.segmenter.initial_segment_count");
public static ConfigElementKey FFmpegUseExperimentalTranscoder => new("ffmpeg.use_experimental_transcoder");
public static ConfigElementKey SearchIndexVersion => new("search_index.version"); public static ConfigElementKey SearchIndexVersion => new("search_index.version");
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count"); public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size"); public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");
+1
View File
@@ -24,6 +24,7 @@ namespace ErsatzTV.Core.Domain
public int AudioChannels { get; set; } public int AudioChannels { get; set; }
public int AudioSampleRate { get; set; } public int AudioSampleRate { get; set; }
public bool NormalizeAudio { get; set; } public bool NormalizeAudio { get; set; }
public bool NormalizeFramerate { get; set; }
public static FFmpegProfile New(string name, Resolution resolution) => public static FFmpegProfile New(string name, Resolution resolution) =>
new() new()
+1 -4
View File
@@ -6,10 +6,7 @@ namespace ErsatzTV.Core.Domain
{ {
public class PlayoutAnchor public class PlayoutAnchor
{ {
public int NextScheduleItemId { get; set; } public CollectionEnumeratorState ScheduleItemsEnumeratorState { get; set; }
public ProgramScheduleItem NextScheduleItem { get; set; }
public DateTime NextStart { get; set; } public DateTime NextStart { get; set; }
public int? MultipleRemaining { get; set; } public int? MultipleRemaining { get; set; }
public DateTime? DurationFinish { get; set; } public DateTime? DurationFinish { get; set; }
+1
View File
@@ -8,6 +8,7 @@ namespace ErsatzTV.Core.Domain
public string Name { get; set; } public string Name { get; set; }
public bool KeepMultiPartEpisodesTogether { get; set; } public bool KeepMultiPartEpisodesTogether { get; set; }
public bool TreatCollectionsAsShows { get; set; } public bool TreatCollectionsAsShows { get; set; }
public bool ShuffleScheduleItems { get; set; }
public List<ProgramScheduleItem> Items { get; set; } public List<ProgramScheduleItem> Items { get; set; }
public List<Playout> Playouts { get; set; } public List<Playout> Playouts { get; set; }
} }
@@ -1,10 +0,0 @@
namespace ErsatzTV.Core.Errors
{
public class MediaSourceInaccessible : BaseError
{
public MediaSourceInaccessible()
: base("Media source is not accessible or missing")
{
}
}
}
+7 -2
View File
@@ -6,9 +6,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Flurl" Version="3.0.2" /> <PackageReference Include="Flurl" Version="3.0.4" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" /> <PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="MediatR" Version="9.0.0" /> <PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
@@ -25,5 +26,9 @@
<_Parameter1>ErsatzTV.Core.Tests</_Parameter1> <_Parameter1>ErsatzTV.Core.Tests</_Parameter1>
</AssemblyAttribute> </AssemblyAttribute>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.FFmpeg\ErsatzTV.FFmpeg.csproj" />
</ItemGroup>
</Project> </Project>
+2 -2
View File
@@ -4,7 +4,7 @@
{ {
public override string ToString() => public override string ToString() =>
$@"ffconcat version 1.0 $@"ffconcat version 1.0
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber} file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode=ts-legacy
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}"; file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode=ts-legacy";
} }
} }
+215 -101
View File
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg.State;
using LanguageExt; using LanguageExt;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
@@ -22,8 +23,10 @@ namespace ErsatzTV.Core.FFmpeg
private IDisplaySize _resolution; private IDisplaySize _resolution;
private Option<IDisplaySize> _scaleToSize = None; private Option<IDisplaySize> _scaleToSize = None;
private Option<ChannelWatermark> _watermark; private Option<ChannelWatermark> _watermark;
private Option<List<FadePoint>> _maybeFadePoints = None;
private Option<int> _watermarkIndex; private Option<int> _watermarkIndex;
private string _pixelFormat; private string _pixelFormat;
private string _videoDecoder;
private string _videoEncoder; private string _videoEncoder;
private Option<string> _subtitle; private Option<string> _subtitle;
private bool _boxBlur; private bool _boxBlur;
@@ -74,6 +77,12 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegComplexFilterBuilder WithDecoder(string decoder)
{
_videoDecoder = decoder;
return this;
}
public FFmpegComplexFilterBuilder WithInputPixelFormat(Option<string> maybePixelFormat) public FFmpegComplexFilterBuilder WithInputPixelFormat(Option<string> maybePixelFormat)
{ {
foreach (string pixelFormat in maybePixelFormat) foreach (string pixelFormat in maybePixelFormat)
@@ -86,10 +95,12 @@ namespace ErsatzTV.Core.FFmpeg
public FFmpegComplexFilterBuilder WithWatermark( public FFmpegComplexFilterBuilder WithWatermark(
Option<ChannelWatermark> watermark, Option<ChannelWatermark> watermark,
Option<List<FadePoint>> maybeFadePoints,
IDisplaySize resolution, IDisplaySize resolution,
Option<int> watermarkIndex) Option<int> watermarkIndex)
{ {
_watermark = watermark; _watermark = watermark;
_maybeFadePoints = maybeFadePoints;
_resolution = resolution; _resolution = resolution;
_watermarkIndex = watermarkIndex; _watermarkIndex = watermarkIndex;
return this; return this;
@@ -133,29 +144,45 @@ namespace ErsatzTV.Core.FFmpeg
public Option<FFmpegComplexFilter> Build(bool videoOnly, int videoInput, int videoStreamIndex, int audioInput, Option<int> audioStreamIndex, bool isSong) public Option<FFmpegComplexFilter> Build(bool videoOnly, int videoInput, int videoStreamIndex, int audioInput, Option<int> audioStreamIndex, bool isSong)
{ {
// since .Contains is used on pixel format, we need it to be not null
_pixelFormat ??= string.Empty;
var complexFilter = new StringBuilder(); var complexFilter = new StringBuilder();
var videoLabel = $"{videoInput}:{(isSong ? "v" : videoStreamIndex.ToString())}"; string videoLabel = $"{videoInput}:{(isSong ? "v" : videoStreamIndex.ToString())}";
string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a"); string audioLabel = audioStreamIndex.Match(index => $"{audioInput}:{index}", () => "0:a");
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None); HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
bool isHardwareDecode = acceleration switch bool isHardwareDecode = acceleration switch
{ {
HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4", HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4" &&
(_deinterlace == false || !_pixelFormat.Contains("p10le")),
// we need an initial hwupload_cuda when only padding with these pixel formats // we need an initial hwupload_cuda when only padding with these pixel formats
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome => HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome =>
!isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"), !isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"),
HardwareAccelerationKind.Nvenc => !isSong, HardwareAccelerationKind.Nvenc => !isSong &&
(string.IsNullOrWhiteSpace(_videoDecoder) ||
_videoDecoder.Contains("cuvid")),
HardwareAccelerationKind.Qsv => !isSong, HardwareAccelerationKind.Qsv => !isSong,
HardwareAccelerationKind.VideoToolbox => false, HardwareAccelerationKind.VideoToolbox => false,
_ => false _ => false
}; };
bool nvencDeinterlace = acceleration == HardwareAccelerationKind.Nvenc && _videoDecoder == "mpeg2_cuvid" &&
_deinterlace;
// mpeg2_cuvid will handle deinterlace and is "not" a hardware decode
if (nvencDeinterlace)
{
_deinterlace = false;
isHardwareDecode = false;
}
var audioFilterQueue = new List<string>(); var audioFilterQueue = new List<string>();
var videoFilterQueue = new List<string>(); var videoFilterQueue = new List<string>();
string watermarkPreprocess = string.Empty; var watermarkPreprocess = new List<string>();
string watermarkOverlay = string.Empty; string watermarkOverlay = string.Empty;
if (_normalizeLoudness) if (_normalizeLoudness)
@@ -209,15 +236,17 @@ namespace ErsatzTV.Core.FFmpeg
if (_deinterlace) if (_deinterlace)
{ {
string filter = acceleration switch Option<string> maybeFilter = acceleration switch
{ {
HardwareAccelerationKind.Qsv => "deinterlace_qsv", HardwareAccelerationKind.Qsv => "deinterlace_qsv",
HardwareAccelerationKind.Nvenc when !usesHardwareFilters && _pixelFormat.Contains("p10le") =>
"hwupload_cuda,yadif_cuda",
HardwareAccelerationKind.Nvenc => "yadif_cuda", HardwareAccelerationKind.Nvenc => "yadif_cuda",
HardwareAccelerationKind.Vaapi => "deinterlace_vaapi", HardwareAccelerationKind.Vaapi => "deinterlace_vaapi",
_ => "yadif=1" _ => "yadif=1"
}; };
if (!string.IsNullOrWhiteSpace(filter)) foreach (string filter in maybeFilter)
{ {
videoFilterQueue.Add(filter); videoFilterQueue.Add(filter);
} }
@@ -225,9 +254,9 @@ namespace ErsatzTV.Core.FFmpeg
string[] h264hevc = { "h264", "hevc" }; string[] h264hevc = { "h264", "hevc" };
if (acceleration == HardwareAccelerationKind.Vaapi && (_pixelFormat ?? string.Empty).EndsWith("p10le") && if (_deinterlace == false && acceleration == HardwareAccelerationKind.Vaapi &&
h264hevc.Contains(_inputCodec) (_pixelFormat ?? string.Empty).EndsWith("p10le") &&
&& (_pixelFormat != "yuv420p10le" || _inputCodec != "hevc")) h264hevc.Contains(_inputCodec) && (_pixelFormat != "yuv420p10le" || _inputCodec != "hevc"))
{ {
videoFilterQueue.Add("format=p010le,format=nv12|vaapi,hwupload"); videoFilterQueue.Add("format=p010le,format=nv12|vaapi,hwupload");
} }
@@ -237,13 +266,155 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add("format=nv12|vaapi,hwupload"); videoFilterQueue.Add("format=nv12|vaapi,hwupload");
} }
bool scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome;
bool usesSoftwareFilters = _padToSize.IsSome || _watermark.IsSome;
bool hasFadePoints = _maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0;
var softwareFilterQueue = new List<string>();
if (usesSoftwareFilters)
{
if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters))
{
Option<string> maybeFormat = acceleration switch
{
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
HardwareAccelerationKind.Nvenc when _padToSize.IsNone || nvencDeinterlace => None,
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
"format=p010le,format=nv12",
HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p",
_ when isSong => "format=yuv420p",
_ => "format=nv12"
};
foreach (string format in maybeFormat)
{
softwareFilterQueue.Add("hwdownload");
softwareFilterQueue.Add(format);
}
if (nvencDeinterlace)
{
softwareFilterQueue.Add("hwdownload");
}
}
if (_boxBlur)
{
softwareFilterQueue.Add("boxblur=40");
}
if (videoOnly)
{
softwareFilterQueue.Add("deband");
}
foreach (ChannelWatermark watermark in _watermark)
{
Option<string> maybeFormats = acceleration switch
{
// overlay_cuda only supports alpha with yuva420p
HardwareAccelerationKind.Nvenc => "yuva420p",
_ when watermark.Opacity != 100 || hasFadePoints =>
"yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8",
_ => None
};
foreach (string formats in maybeFormats)
{
watermarkPreprocess.Add($"format={formats}");
}
double horizontalMargin = Math.Round(watermark.HorizontalMarginPercent / 100.0 * _resolution.Width);
double verticalMargin = Math.Round(watermark.VerticalMarginPercent / 100.0 * _resolution.Height);
string position = watermark.Location switch
{
WatermarkLocation.BottomLeft => $"x={horizontalMargin}:y=H-h-{verticalMargin}",
WatermarkLocation.TopLeft => $"x={horizontalMargin}:y={verticalMargin}",
WatermarkLocation.TopRight => $"x=W-w-{horizontalMargin}:y={verticalMargin}",
WatermarkLocation.TopMiddle => $"x=(W-w)/2:y={verticalMargin}",
WatermarkLocation.RightMiddle => $"x=W-w-{horizontalMargin}:y=(H-h)/2",
WatermarkLocation.BottomMiddle => $"x=(W-w)/2:y=H-h-{verticalMargin}",
WatermarkLocation.LeftMiddle => $"x={horizontalMargin}:y=(H-h)/2",
_ => $"x=W-w-{horizontalMargin}:y=H-h-{verticalMargin}"
};
if (watermark.Opacity != 100)
{
double opacity = watermark.Opacity / 100.0;
watermarkPreprocess.Add($"colorchannelmixer=aa={opacity:F2}");
}
if (watermark.Size == WatermarkSize.Scaled)
{
double width = Math.Round(watermark.WidthPercent / 100.0 * _resolution.Width);
watermarkPreprocess.Add($"scale={width}:-1");
}
foreach (List<FadePoint> fadePoints in _maybeFadePoints)
{
watermarkPreprocess.AddRange(fadePoints.Map(fp => fp.ToFilter()));
}
if (acceleration == HardwareAccelerationKind.Nvenc)
{
watermarkPreprocess.Add("hwupload_cuda");
}
watermarkOverlay = acceleration switch
{
HardwareAccelerationKind.Nvenc => $"overlay_cuda={position}",
_ => $"overlay={position}"
};
if (hasFadePoints && acceleration != HardwareAccelerationKind.Nvenc)
{
watermarkOverlay += "," + acceleration switch
{
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
_ when isSong => "format=yuv420p",
_ => "format=nv12"
};
}
}
}
string outputPixelFormat = null;
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
{
switch (acceleration, _videoEncoder, _pixelFormat)
{
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv420p10le"):
outputPixelFormat = "yuv420p";
break;
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv444p10le"):
outputPixelFormat = "yuv444p";
break;
}
}
string outputFormat = (_videoEncoder, _pixelFormat) switch
{
("hevc_nvenc", "yuv420p10le") => "p010le",
("h264_nvenc", "yuv420p10le") => "p010le",
_ => null
};
_scaleToSize.IfSome( _scaleToSize.IfSome(
size => size =>
{ {
string filter = acceleration switch string filter = acceleration switch
{ {
HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}", HardwareAccelerationKind.Qsv => $"scale_qsv=w={size.Width}:h={size.Height}",
HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" => HardwareAccelerationKind.Nvenc when _watermark.IsSome && _scaleToSize.IsNone =>
$"format=yuv420p,hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc when _watermark.IsSome && _padToSize.IsNone =>
$"scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc when _watermark.IsNone && !string.IsNullOrEmpty(outputFormat) =>
$"scale_cuda={size.Width}:{size.Height}:format={outputFormat}",
HardwareAccelerationKind.Nvenc when _pixelFormat is "yuv420p10le" && usesHardwareFilters == false =>
$"hwupload_cuda,scale_cuda={size.Width}:{size.Height}", $"hwupload_cuda,scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}", HardwareAccelerationKind.Nvenc => $"scale_cuda={size.Width}:{size.Height}",
HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}", HardwareAccelerationKind.Vaapi => $"scale_vaapi=format=nv12:w={size.Width}:h={size.Height}",
@@ -257,93 +428,45 @@ namespace ErsatzTV.Core.FFmpeg
} }
}); });
bool scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome;
bool usesSoftwareFilters = _padToSize.IsSome || _watermark.IsSome;
if (scaleOrPad && _boxBlur == false) if (scaleOrPad && _boxBlur == false)
{ {
if (acceleration == HardwareAccelerationKind.Nvenc)
{
if (!isHardwareDecode && !string.IsNullOrWhiteSpace(outputPixelFormat))
{
videoFilterQueue.Add($"hwdownload,format={outputPixelFormat}");
}
}
videoFilterQueue.Add("setsar=1"); videoFilterQueue.Add("setsar=1");
} }
if (usesSoftwareFilters) videoFilterQueue.AddRange(softwareFilterQueue);
{
if (acceleration != HardwareAccelerationKind.None && (isHardwareDecode || usesHardwareFilters))
{
videoFilterQueue.Add("hwdownload");
string format = acceleration switch
{
HardwareAccelerationKind.Vaapi => "format=nv12|vaapi",
HardwareAccelerationKind.Nvenc when _pixelFormat == "yuv420p10le" =>
"format=p010le,format=nv12",
HardwareAccelerationKind.Qsv when isSong => "format=nv12,format=yuv420p",
_ when isSong => "format=yuv420p",
_ => "format=nv12"
};
videoFilterQueue.Add(format);
}
if (_boxBlur)
{
videoFilterQueue.Add("boxblur=40");
}
if (videoOnly)
{
videoFilterQueue.Add("deband");
}
foreach (ChannelWatermark watermark in _watermark)
{
string enable = watermark.Mode == ChannelWatermarkMode.Intermittent
? $":enable='lt(mod(mod(time(0),60*60),{watermark.FrequencyMinutes}*60),{watermark.DurationSeconds})'"
: string.Empty;
double horizontalMargin = Math.Round(watermark.HorizontalMarginPercent / 100.0 * _resolution.Width);
double verticalMargin = Math.Round(watermark.VerticalMarginPercent / 100.0 * _resolution.Height);
string position = watermark.Location switch
{
ChannelWatermarkLocation.BottomLeft => $"x={horizontalMargin}:y=H-h-{verticalMargin}",
ChannelWatermarkLocation.TopLeft => $"x={horizontalMargin}:y={verticalMargin}",
ChannelWatermarkLocation.TopRight => $"x=W-w-{horizontalMargin}:y={verticalMargin}",
ChannelWatermarkLocation.TopMiddle => $"x=(W-w)/2:y={verticalMargin}",
ChannelWatermarkLocation.RightMiddle => $"x=W-w-{horizontalMargin}:y=(H-h)/2",
ChannelWatermarkLocation.BottomMiddle => $"x=(W-w)/2:y=H-h-{verticalMargin}",
ChannelWatermarkLocation.LeftMiddle => $"x={horizontalMargin}:y=(H-h)/2",
_ => $"x=W-w-{horizontalMargin}:y=H-h-{verticalMargin}"
};
if (watermark.Size == ChannelWatermarkSize.Scaled)
{
double width = Math.Round(watermark.WidthPercent / 100.0 * _resolution.Width);
watermarkPreprocess = $"scale={width}:-1";
}
if (watermark.Opacity != 100)
{
const string FORMATS = "yuva420p|yuva444p|yuva422p|rgba|abgr|bgra|gbrap|ya8";
string join = string.Empty;
double opacity = watermark.Opacity / 100.0;
if (!string.IsNullOrWhiteSpace(watermarkPreprocess))
{
join = ",";
}
watermarkPreprocess = $"format={FORMATS},colorchannelmixer=aa={opacity:F2}{join}{watermarkPreprocess}";
}
watermarkOverlay = $"overlay={position}{enable}";
}
}
_padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2")); _padToSize.IfSome(size => videoFilterQueue.Add($"pad={size.Width}:{size.Height}:(ow-iw)/2:(oh-ih)/2"));
if (acceleration == HardwareAccelerationKind.Nvenc && _watermark.IsSome)
{
if (_scaleToSize.IsSome)
{
videoFilterQueue.Add("hwdownload,format=nv12,format=yuv420p");
videoFilterQueue.Add("hwupload_cuda");
}
else if (_padToSize.IsNone)
{
videoFilterQueue.Add("scale_cuda=format=yuv420p");
}
else
{
videoFilterQueue.Add("format=yuv420p");
videoFilterQueue.Add("hwupload_cuda");
}
}
foreach (string subtitle in _subtitle) foreach (string subtitle in _subtitle)
{ {
videoFilterQueue.Add(subtitle); videoFilterQueue.Add(subtitle);
} }
string outputPixelFormat = null;
if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None && if (usesSoftwareFilters && acceleration != HardwareAccelerationKind.None &&
string.IsNullOrWhiteSpace(watermarkOverlay)) string.IsNullOrWhiteSpace(watermarkOverlay))
@@ -356,19 +479,6 @@ namespace ErsatzTV.Core.FFmpeg
videoFilterQueue.Add(upload); videoFilterQueue.Add(upload);
} }
if (!usesSoftwareFilters && string.IsNullOrWhiteSpace(watermarkOverlay))
{
switch (acceleration, _videoEncoder, _pixelFormat)
{
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv420p10le"):
outputPixelFormat = "yuv420p";
break;
case (HardwareAccelerationKind.Nvenc, "h264_nvenc", "yuv444p10le"):
outputPixelFormat = "yuv444p";
break;
}
}
bool hasAudioFilters = audioFilterQueue.Any(); bool hasAudioFilters = audioFilterQueue.Any();
if (hasAudioFilters) if (hasAudioFilters)
{ {
@@ -412,9 +522,10 @@ namespace ErsatzTV.Core.FFmpeg
watermarkLabel = $"[{audioInput+1}:{index}]"; watermarkLabel = $"[{audioInput+1}:{index}]";
} }
if (!string.IsNullOrWhiteSpace(watermarkPreprocess)) if (watermarkPreprocess.Count > 0)
{ {
complexFilter.Append($"{watermarkLabel}{watermarkPreprocess}[wmp];"); var joined = string.Join(",", watermarkPreprocess);
complexFilter.Append($"{watermarkLabel}{joined}[wmp];");
watermarkLabel = "[wmp]"; watermarkLabel = "[wmp]";
} }
@@ -430,6 +541,9 @@ namespace ErsatzTV.Core.FFmpeg
case (true, HardwareAccelerationKind.Nvenc): case (true, HardwareAccelerationKind.Nvenc):
complexFilter.Append(",hwupload_cuda"); complexFilter.Append(",hwupload_cuda");
break; break;
// no need to upload since we're already in the GPU with overlay_cuda
case (_, HardwareAccelerationKind.Nvenc) when scaleOrPad == false && _watermark.IsSome:
break;
case (_, HardwareAccelerationKind.Qsv): case (_, HardwareAccelerationKind.Qsv):
complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64"); complexFilter.Append(",format=yuv420p,hwupload=extra_hw_frames=64");
break; break;
@@ -0,0 +1,410 @@
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.OutputFormat;
using ErsatzTV.FFmpeg.State;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using FFmpegState = ErsatzTV.FFmpeg.FFmpegState;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
namespace ErsatzTV.Core.FFmpeg;
public class FFmpegLibraryProcessService : IFFmpegProcessService
{
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly ILogger<FFmpegLibraryProcessService> _logger;
public FFmpegLibraryProcessService(
FFmpegProcessService ffmpegProcessService,
FFmpegPlaybackSettingsCalculator playbackSettingsCalculator,
IFFmpegStreamSelector ffmpegStreamSelector,
ILogger<FFmpegLibraryProcessService> logger)
{
_ffmpegProcessService = ffmpegProcessService;
_playbackSettingsCalculator = playbackSettingsCalculator;
_ffmpegStreamSelector = ffmpegStreamSelector;
_logger = logger;
}
public async Task<Process> ForPlayoutItem(
string ffmpegPath,
bool saveReports,
Channel channel,
MediaVersion videoVersion,
MediaVersion audioVersion,
string videoPath,
string audioPath,
DateTimeOffset start,
DateTimeOffset finish,
DateTimeOffset now,
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
long ptsOffset,
Option<int> targetFramerate)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode,
channel.FFmpegProfile,
videoVersion,
videoStream,
maybeAudioStream,
start,
now,
inPoint,
outPoint,
hlsRealtime,
targetFramerate);
Option<WatermarkOptions> watermarkOptions =
await _ffmpegProcessService.GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
Option<List<FadePoint>> maybeFadePoints = watermarkOptions
.Map(o => o.Watermark)
.Flatten()
.Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
.Map(
wm =>
WatermarkCalculator.CalculateFadePoints(
start,
inPoint,
outPoint,
playbackSettings.StreamSeek,
wm.FrequencyMinutes,
wm.DurationSeconds));
var audioState = new AudioState(
playbackSettings.AudioCodec,
playbackSettings.AudioChannels,
playbackSettings.AudioBitrate,
playbackSettings.AudioBufferSize,
playbackSettings.AudioSampleRate,
videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None,
playbackSettings.NormalizeLoudness);
var ffmpegVideoStream = new VideoStream(
videoStream.Index,
videoStream.Codec,
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.RFrameRate,
videoPath != audioPath); // still image when paths are different
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
Option<AudioInputFile> audioInputFile = maybeAudioStream.Map(
audioStream =>
{
var ffmpegAudioStream = new AudioStream(audioStream.Index, audioStream.Codec, audioStream.Channels);
return new AudioInputFile(audioPath, new List<AudioStream> { ffmpegAudioStream }, audioState);
});
var watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);
// TODO: need formats for these codecs
string videoFormat = playbackSettings.VideoCodec switch
{
"libx265" or "hevc_nvenc" or "hevc_qsv" or "hevc_vaapi" or "hevc_videotoolbox" => VideoFormat.Hevc,
"libx264" or "h264_nvenc" or "h264_qsv" or "h264_vaapi" or "h264_videotoolbox" => VideoFormat.H264,
"mpeg2video" => VideoFormat.Mpeg2Video,
"copy" => VideoFormat.Copy,
_ => throw new ArgumentOutOfRangeException($"unexpected video codec {playbackSettings.VideoCodec}")
};
HardwareAccelerationMode hwAccel = playbackSettings.HardwareAcceleration switch
{
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
_ => HardwareAccelerationMode.None
};
OutputFormatKind outputFormat = channel.StreamingMode == StreamingMode.HttpLiveStreamingSegmenter
? OutputFormatKind.Hls
: OutputFormatKind.MpegTs;
Option<string> hlsPlaylistPath = outputFormat == OutputFormatKind.Hls
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8")
: Option<string>.None;
Option<string> hlsSegmentTemplate = outputFormat == OutputFormatKind.Hls
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
: Option<string>.None;
// normalize songs to yuv420p
Option<IPixelFormat> desiredPixelFormat =
videoPath == audioPath ? ffmpegVideoStream.PixelFormat : new PixelFormatYuv420P();
var desiredState = new FrameState(
playbackSettings.RealtimeOutput,
false, // TODO: fallback filler needs to loop
videoFormat,
desiredPixelFormat,
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
playbackSettings.VideoTrackTimeScale,
playbackSettings.Deinterlace);
var ffmpegState = new FFmpegState(
saveReports,
hwAccel,
VaapiDriverName(hwAccel, vaapiDriver),
VaapiDeviceName(hwAccel, vaapiDevice),
playbackSettings.StreamSeek,
finish - now,
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
"ErsatzTV",
channel.Name,
maybeAudioStream.Map(s => Optional(s.Language)).Flatten(),
outputFormat,
hlsPlaylistPath,
hlsSegmentTemplate,
ptsOffset);
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
var pipelineBuilder = new PipelineBuilder(
videoInputFile,
audioInputFile,
watermarkInputFile,
FileSystemLayout.FFmpegReportsFolder,
_logger);
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
return GetProcess(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
}
private Option<WatermarkInputFile> GetWatermarkInputFile(
Option<WatermarkOptions> watermarkOptions,
Option<List<FadePoint>> maybeFadePoints)
{
foreach (WatermarkOptions options in watermarkOptions)
{
foreach (ChannelWatermark watermark in options.Watermark)
{
// skip watermark if intermittent and no fade points
if (watermark.Mode != ChannelWatermarkMode.None &&
(watermark.Mode != ChannelWatermarkMode.Intermittent ||
maybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)))
{
foreach (string path in options.ImagePath)
{
var watermarkInputFile = new WatermarkInputFile(
path,
new List<VideoStream>
{
new(
options.ImageStreamIndex.IfNone(0),
"unknown",
new PixelFormatUnknown(),
new FrameSize(1, 1),
Option<string>.None,
!options.IsAnimated)
},
new WatermarkState(
maybeFadePoints.Map(
lst => lst.Map(
fp =>
{
return fp switch
{
FadeInPoint fip => (WatermarkFadePoint)new WatermarkFadeIn(
fip.Time,
fip.EnableStart,
fip.EnableFinish),
FadeOutPoint fop => new WatermarkFadeOut(
fop.Time,
fop.EnableStart,
fop.EnableFinish),
_ => throw new NotSupportedException() // this will never happen
};
}).ToList()),
watermark.Location,
watermark.Size,
watermark.WidthPercent,
watermark.HorizontalMarginPercent,
watermark.VerticalMarginPercent,
watermark.Opacity));
return watermarkInputFile;
}
}
}
}
return None;
}
public Task<Process> ForError(
string ffmpegPath,
Channel channel,
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime,
long ptsOffset) =>
_ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset);
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
var concatInputFile = new ConcatInputFile(
$"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}",
resolution);
var pipelineBuilder = new PipelineBuilder(None, None, None, FileSystemLayout.FFmpegReportsFolder, _logger);
FFmpegPipeline pipeline = pipelineBuilder.Concat(
concatInputFile,
FFmpegState.Concat(saveReports, channel.Name));
return GetProcess(ffmpegPath, None, None, None, concatInputFile, pipeline);
}
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) =>
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host);
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) =>
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile);
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) =>
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile);
public Task<Either<BaseError, string>> GenerateSongImage(
string ffmpegPath,
Option<string> subtitleFile,
Channel channel,
Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion,
string videoPath,
bool boxBlur,
Option<string> watermarkPath,
WatermarkLocation watermarkLocation,
int horizontalMarginPercent,
int verticalMarginPercent,
int watermarkWidthPercent) =>
_ffmpegProcessService.GenerateSongImage(
ffmpegPath,
subtitleFile,
channel,
globalWatermark,
videoVersion,
videoPath,
boxBlur,
watermarkPath,
watermarkLocation,
horizontalMarginPercent,
verticalMarginPercent,
watermarkWidthPercent);
private Process GetProcess(
string ffmpegPath,
Option<VideoInputFile> videoInputFile,
Option<AudioInputFile> audioInputFile,
Option<WatermarkInputFile> watermarkInputFile,
Option<ConcatInputFile> concatInputFile,
FFmpegPipeline pipeline)
{
IEnumerable<string> loggedSteps = pipeline.PipelineSteps.Map(ps => ps.GetType().Name);
IEnumerable<string> loggedVideoFilters =
videoInputFile.Map(f => f.FilterSteps.Map(vf => vf.GetType().Name)).Flatten();
IEnumerable<string> loggedAudioFilters =
audioInputFile.Map(f => f.FilterSteps.Map(af => af.GetType().Name)).Flatten();
_logger.LogDebug(
"FFmpeg pipeline {PipelineSteps}, {AudioFilters}, {VideoFilters}",
loggedSteps,
loggedAudioFilters,
loggedVideoFilters
);
IList<EnvironmentVariable> environmentVariables =
CommandGenerator.GenerateEnvironmentVariables(pipeline.PipelineSteps);
IList<string> arguments = CommandGenerator.GenerateArguments(
videoInputFile,
audioInputFile,
watermarkInputFile,
concatInputFile,
pipeline.PipelineSteps);
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
if (environmentVariables.Any())
{
_logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables);
}
foreach ((string key, string value) in environmentVariables)
{
startInfo.EnvironmentVariables[key] = value;
}
foreach (string argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
return new Process
{
StartInfo = startInfo
};
}
private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver)
{
if (accelerationMode == HardwareAccelerationMode.Vaapi)
{
switch (driver)
{
case VaapiDriver.i965:
return "i965";
case VaapiDriver.iHD:
return "iHD";
case VaapiDriver.RadeonSI:
return "radeonsi";
}
}
return Option<string>.None;
}
private static Option<string> VaapiDeviceName(HardwareAccelerationMode accelerationMode, string vaapiDevice)
{
return accelerationMode == HardwareAccelerationMode.Vaapi ? vaapiDevice : Option<string>.None;
}
}
@@ -12,7 +12,7 @@ namespace ErsatzTV.Core.FFmpeg
public List<string> FormatFlags { get; set; } public List<string> FormatFlags { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; } public HardwareAccelerationKind HardwareAcceleration { get; set; }
public string VideoDecoder { get; set; } public string VideoDecoder { get; set; }
public bool RealtimeOutput => true; public bool RealtimeOutput { get; set; }
public Option<TimeSpan> StreamSeek { get; set; } public Option<TimeSpan> StreamSeek { get; set; }
public Option<IDisplaySize> ScaledSize { get; set; } public Option<IDisplaySize> ScaledSize { get; set; }
public bool PadToDesiredResolution { get; set; } public bool PadToDesiredResolution { get; set; }
@@ -28,5 +28,6 @@ namespace ErsatzTV.Core.FFmpeg
public bool Deinterlace { get; set; } public bool Deinterlace { get; set; }
public Option<int> VideoTrackTimeScale { get; set; } public Option<int> VideoTrackTimeScale { get; set; }
public bool NormalizeLoudness { get; set; } public bool NormalizeLoudness { get; set; }
public Option<int> FrameRate { get; set; }
} }
} }
@@ -36,6 +36,12 @@ namespace ErsatzTV.Core.FFmpeg
"+igndts" "+igndts"
}; };
private static readonly List<string> SegmenterFormatFlags = new()
{
"+discardcorrupt",
"+igndts"
};
public FFmpegPlaybackSettings ConcatSettings => new() public FFmpegPlaybackSettings ConcatSettings => new()
{ {
ThreadCount = 1, ThreadCount = 1,
@@ -51,14 +57,27 @@ namespace ErsatzTV.Core.FFmpeg
DateTimeOffset start, DateTimeOffset start,
DateTimeOffset now, DateTimeOffset now,
TimeSpan inPoint, TimeSpan inPoint,
TimeSpan outPoint) TimeSpan outPoint,
bool hlsRealtime,
Option<int> targetFramerate)
{ {
var result = new FFmpegPlaybackSettings var result = new FFmpegPlaybackSettings
{ {
ThreadCount = ffmpegProfile.ThreadCount, FormatFlags = streamingMode switch
FormatFlags = CommonFormatFlags {
StreamingMode.HttpLiveStreamingSegmenter => SegmenterFormatFlags,
_ => CommonFormatFlags,
},
RealtimeOutput = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenter => hlsRealtime,
_ => true
}
}; };
// always use one thread with realtime output
result.ThreadCount = result.RealtimeOutput ? 1 : ffmpegProfile.ThreadCount;
if (now != start || inPoint != TimeSpan.Zero) if (now != start || inPoint != TimeSpan.Zero)
{ {
result.StreamSeek = now - start + inPoint; result.StreamSeek = now - start + inPoint;
@@ -95,6 +114,11 @@ namespace ErsatzTV.Core.FFmpeg
if (ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo) if (ffmpegProfile.Transcode && ffmpegProfile.NormalizeVideo)
{ {
if (ffmpegProfile.NormalizeFramerate)
{
result.FrameRate = targetFramerate;
}
result.VideoTrackTimeScale = 90000; result.VideoTrackTimeScale = 90000;
} }
+107 -40
View File
@@ -21,6 +21,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -213,30 +214,62 @@ namespace ErsatzTV.Core.FFmpeg
return this; return this;
} }
public FFmpegProcessBuilder WithFrameRate(Option<int> frameRate)
{
foreach (int fr in frameRate)
{
_arguments.Add("-r");
_arguments.Add($"{fr}");
_arguments.Add("-vsync");
_arguments.Add("cfr");
}
return this;
}
public FFmpegProcessBuilder WithWatermark( public FFmpegProcessBuilder WithWatermark(
Option<WatermarkOptions> watermarkOptions, Option<WatermarkOptions> watermarkOptions,
Option<List<FadePoint>> maybeFadePoints,
IDisplaySize resolution) IDisplaySize resolution)
{ {
foreach (WatermarkOptions options in watermarkOptions) ChannelWatermarkMode maybeWatermarkMode = watermarkOptions.Map(wmo => wmo.Watermark.Map(wm => wm.Mode)).Flatten()
.IfNone(ChannelWatermarkMode.None);
// skip watermark if intermittent and no fade points
if (maybeWatermarkMode != ChannelWatermarkMode.None &&
(maybeWatermarkMode != ChannelWatermarkMode.Intermittent ||
maybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)))
{ {
foreach (string path in options.ImagePath) foreach (WatermarkOptions options in watermarkOptions)
{ {
if (options.IsAnimated) foreach (string path in options.ImagePath)
{ {
_arguments.Add("-ignore_loop"); if (options.IsAnimated)
_arguments.Add("0"); {
_arguments.Add("-ignore_loop");
_arguments.Add("0");
}
// when we have fade points, we need to loop the static watermark image
else if (maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0)
{
_arguments.Add("-stream_loop");
_arguments.Add("-1");
}
_arguments.Add("-i");
_arguments.Add(path);
_complexFilterBuilder = _complexFilterBuilder.WithWatermark(
options.Watermark,
maybeFadePoints,
resolution,
options.ImageStreamIndex);
} }
_arguments.Add("-i");
_arguments.Add(path);
_complexFilterBuilder = _complexFilterBuilder.WithWatermark(
options.Watermark,
resolution,
options.ImageStreamIndex);
} }
} }
return this; return this;
} }
@@ -253,7 +286,8 @@ namespace ErsatzTV.Core.FFmpeg
string audioPath, string audioPath,
string decoder, string decoder,
Option<string> codec, Option<string> codec,
Option<string> pixelFormat) Option<string> pixelFormat,
bool deinterlace)
{ {
if (audioPath == videoPath) if (audioPath == videoPath)
{ {
@@ -273,6 +307,15 @@ namespace ErsatzTV.Core.FFmpeg
{ {
_arguments.Add("-c:v"); _arguments.Add("-c:v");
_arguments.Add(decoder); _arguments.Add(decoder);
if (decoder == "mpeg2_cuvid" && deinterlace)
{
_arguments.Add("-deint");
_arguments.Add("2");
}
_complexFilterBuilder = _complexFilterBuilder
.WithDecoder(decoder);
} }
_complexFilterBuilder = _complexFilterBuilder _complexFilterBuilder = _complexFilterBuilder
@@ -379,36 +422,33 @@ namespace ErsatzTV.Core.FFmpeg
_arguments.Add($"{format}"); _arguments.Add($"{format}");
return this; return this;
} }
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion) public FFmpegProcessBuilder WithInitialDiscontinuity()
{
_arguments.Add("-mpegts_flags");
_arguments.Add("+initial_discontinuity");
return this;
}
public FFmpegProcessBuilder WithHls(
string channelNumber,
Option<MediaVersion> mediaVersion,
long ptsOffset,
Option<int> maybeTimeScale,
Option<int> maybeFrameRate)
{ {
const int SEGMENT_SECONDS = 4; const int SEGMENT_SECONDS = 4;
int frameRate = maybeFrameRate.IfNone(GetFrameRateFromMediaVersion(mediaVersion));
var frameRate = 24; foreach (int timescale in maybeTimeScale)
foreach (MediaVersion version in mediaVersion)
{ {
if (!int.TryParse(version.RFrameRate, out int fr)) _arguments.Add("-output_ts_offset");
{ _arguments.Add($"{(ptsOffset / (double)timescale).ToString(NumberFormatInfo.InvariantInfo)}");
string[] split = (version.RFrameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
_logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24);
fr = 24;
}
}
frameRate = fr;
} }
_arguments.AddRange( _arguments.AddRange(
new[] new[]
{ {
"-use_wallclock_as_timestamps", "1",
"-g", $"{frameRate * SEGMENT_SECONDS}", "-g", $"{frameRate * SEGMENT_SECONDS}",
"-keyint_min", $"{frameRate * SEGMENT_SECONDS}", "-keyint_min", $"{frameRate * SEGMENT_SECONDS}",
"-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})", "-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})",
@@ -418,7 +458,8 @@ namespace ErsatzTV.Core.FFmpeg
"-segment_list_flags", "+live", "-segment_list_flags", "+live",
"-hls_segment_filename", "-hls_segment_filename",
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live%06d.ts"), Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live%06d.ts"),
"-hls_flags", "program_date_time+append_list+omit_endlist+independent_segments", "-hls_flags", "program_date_time+append_list+discont_start+omit_endlist+independent_segments",
"-mpegts_flags", "+initial_discontinuity",
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8") Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8")
}); });
@@ -602,14 +643,14 @@ namespace ErsatzTV.Core.FFmpeg
} }
}); });
_arguments.Add("-map");
_arguments.Add(videoLabel);
foreach (string _ in audioPath) foreach (string _ in audioPath)
{ {
_arguments.Add("-map"); _arguments.Add("-map");
_arguments.Add(audioLabel); _arguments.Add(audioLabel);
} }
_arguments.Add("-map");
_arguments.Add(videoLabel);
return this; return this;
} }
@@ -678,5 +719,31 @@ namespace ErsatzTV.Core.FFmpeg
StartInfo = startInfo StartInfo = startInfo
}; };
} }
private int GetFrameRateFromMediaVersion(Option<MediaVersion> mediaVersion)
{
var frameRate = 24;
foreach (MediaVersion version in mediaVersion)
{
if (!int.TryParse(version.RFrameRate, out int fr))
{
string[] split = (version.RFrameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
_logger.LogInformation("Unable to detect framerate, using {FrameRate}", 24);
fr = 24;
}
}
frameRate = fr;
}
return frameRate;
}
} }
} }
+61 -28
View File
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -6,9 +7,11 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.FFmpeg.State;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
namespace ErsatzTV.Core.FFmpeg namespace ErsatzTV.Core.FFmpeg
{ {
@@ -51,7 +54,9 @@ namespace ErsatzTV.Core.FFmpeg
bool hlsRealtime, bool hlsRealtime,
FillerKind fillerKind, FillerKind fillerKind,
TimeSpan inPoint, TimeSpan inPoint,
TimeSpan outPoint) TimeSpan outPoint,
long ptsOffset,
Option<int> targetFramerate)
{ {
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion); Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
@@ -65,11 +70,35 @@ namespace ErsatzTV.Core.FFmpeg
start, start,
now, now,
inPoint, inPoint,
outPoint); outPoint,
hlsRealtime,
targetFramerate);
Option<WatermarkOptions> watermarkOptions = Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None); await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
Option<List<FadePoint>> maybeFadePoints = watermarkOptions
.Map(o => o.Watermark)
.Flatten()
.Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
.Map(
wm =>
WatermarkCalculator.CalculateFadePoints(
start,
inPoint,
outPoint,
playbackSettings.StreamSeek,
wm.FrequencyMinutes,
wm.DurationSeconds));
// foreach (List<FadePoint> fadePoints in maybeFadePoints)
// {
// foreach (FadePoint fadePoint in fadePoints)
// {
// _logger.LogDebug("Fade point filter: {FadePointFilter}", fadePoint.ToFilter());
// }
// }
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
.WithThreads(playbackSettings.ThreadCount) .WithThreads(playbackSettings.ThreadCount)
.WithVaapiDriver(vaapiDriver, vaapiDevice) .WithVaapiDriver(vaapiDriver, vaapiDevice)
@@ -87,8 +116,10 @@ namespace ErsatzTV.Core.FFmpeg
audioPath, audioPath,
playbackSettings.VideoDecoder, playbackSettings.VideoDecoder,
videoStream.Codec, videoStream.Codec,
videoStream.PixelFormat) videoStream.PixelFormat,
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution) playbackSettings.Deinterlace)
.WithWatermark(watermarkOptions, maybeFadePoints, channel.FFmpegProfile.Resolution)
.WithFrameRate(playbackSettings.FrameRate)
.WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale) .WithVideoTrackTimeScale(playbackSettings.VideoTrackTimeScale)
.WithAlignedAudio(videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None) .WithAlignedAudio(videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None)
.WithNormalizeLoudness(playbackSettings.NormalizeLoudness); .WithNormalizeLoudness(playbackSettings.NormalizeLoudness);
@@ -153,18 +184,18 @@ namespace ErsatzTV.Core.FFmpeg
.WithMetadata(channel, maybeAudioStream) .WithMetadata(channel, maybeAudioStream)
.WithDuration(finish - now); .WithDuration(finish - now);
switch (channel.StreamingMode) return channel.StreamingMode switch
{ {
// HLS needs to segment and generate playlist // HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter: StreamingMode.HttpLiveStreamingSegmenter => builder.WithHls(
return builder.WithHls(channel.Number, videoVersion) channel.Number,
.WithRealtimeOutput(hlsRealtime) videoVersion,
.Build(); ptsOffset,
default: playbackSettings.VideoTrackTimeScale,
return builder.WithFormat("mpegts") playbackSettings.FrameRate)
.WithPipe() .Build(),
.Build(); _ => builder.WithFormat("mpegts").WithInitialDiscontinuity().WithPipe().Build()
} };
} }
public async Task<Process> ForError( public async Task<Process> ForError(
@@ -172,7 +203,8 @@ namespace ErsatzTV.Core.FFmpeg
Channel channel, Channel channel,
Option<TimeSpan> duration, Option<TimeSpan> duration,
string errorMessage, string errorMessage,
bool hlsRealtime) bool hlsRealtime,
long ptsOffset)
{ {
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettings playbackSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
@@ -220,8 +252,12 @@ namespace ErsatzTV.Core.FFmpeg
{ {
// HLS needs to segment and generate playlist // HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter: case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, None) return builder.WithHls(
.WithRealtimeOutput(hlsRealtime) channel.Number,
None,
ptsOffset,
playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate)
.Build(); .Build();
default: default:
return builder.WithFormat("mpegts") return builder.WithFormat("mpegts")
@@ -255,7 +291,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput) .WithRealtimeOutput(true)
.WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter") .WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter")
.WithMap("0") .WithMap("0")
.WithCopyCodec() .WithCopyCodec()
@@ -295,7 +331,7 @@ namespace ErsatzTV.Core.FFmpeg
string videoPath, string videoPath,
bool boxBlur, bool boxBlur,
Option<string> watermarkPath, Option<string> watermarkPath,
ChannelWatermarkLocation watermarkLocation, WatermarkLocation watermarkLocation,
int horizontalMarginPercent, int horizontalMarginPercent,
int verticalMarginPercent, int verticalMarginPercent,
int watermarkWidthPercent) int watermarkWidthPercent)
@@ -314,7 +350,7 @@ namespace ErsatzTV.Core.FFmpeg
HorizontalMarginPercent = horizontalMarginPercent, HorizontalMarginPercent = horizontalMarginPercent,
VerticalMarginPercent = verticalMarginPercent, VerticalMarginPercent = verticalMarginPercent,
Location = watermarkLocation, Location = watermarkLocation,
Size = ChannelWatermarkSize.Scaled, Size = WatermarkSize.Scaled,
WidthPercent = watermarkWidthPercent, WidthPercent = watermarkWidthPercent,
Opacity = 100 Opacity = 100
} }
@@ -335,14 +371,16 @@ namespace ErsatzTV.Core.FFmpeg
DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch,
DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false,
Option<int>.None);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
.WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur) .WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur)
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution) .WithWatermark(watermarkOptions, None, channel.FFmpegProfile.Resolution)
.WithSubtitleFile(subtitleFile); .WithSubtitleFile(subtitleFile);
foreach (IDisplaySize scaledSize in scalePlaybackSettings.ScaledSize) foreach (IDisplaySize scaledSize in scalePlaybackSettings.ScaledSize)
@@ -384,18 +422,13 @@ namespace ErsatzTV.Core.FFmpeg
private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) => private bool NeedToPad(IDisplaySize target, IDisplaySize displaySize) =>
displaySize.Width != target.Width || displaySize.Height != target.Height; displaySize.Width != target.Width || displaySize.Height != target.Height;
private async Task<WatermarkOptions> GetWatermarkOptions( internal async Task<WatermarkOptions> GetWatermarkOptions(
Channel channel, Channel channel,
Option<ChannelWatermark> globalWatermark, Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion, MediaVersion videoVersion,
Option<ChannelWatermark> watermarkOverride, Option<ChannelWatermark> watermarkOverride,
Option<string> watermarkPath) Option<string> watermarkPath)
{ {
if (videoVersion is BackgroundImageMediaVersion)
{
return new WatermarkOptions(None, None, None, false);
}
if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode && if (channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect && channel.FFmpegProfile.Transcode &&
channel.FFmpegProfile.NormalizeVideo) channel.FFmpegProfile.NormalizeVideo)
{ {
@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.Extensions.DependencyInjection;
namespace ErsatzTV.Core.FFmpeg;
public class FFmpegProcessServiceFactory : IFFmpegProcessServiceFactory
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IServiceProvider _serviceProvider;
public FFmpegProcessServiceFactory(IConfigElementRepository configElementRepository, IServiceProvider serviceProvider)
{
_configElementRepository = configElementRepository;
_serviceProvider = serviceProvider;
}
public async Task<IFFmpegProcessService> GetService()
{
Option<bool> useExperimentalTranscoder =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseExperimentalTranscoder);
return await useExperimentalTranscoder.IfNoneAsync(false)
? _serviceProvider.GetRequiredService<FFmpegLibraryProcessService>()
: _serviceProvider.GetRequiredService<FFmpegProcessService>();
}
}
+23
View File
@@ -0,0 +1,23 @@
using System;
using System.Globalization;
namespace ErsatzTV.Core.FFmpeg;
public abstract record FadePoint(TimeSpan Time, string InOut)
{
public TimeSpan EnableStart { get; set; }
public TimeSpan EnableFinish { get; set; }
public string ToFilter()
{
var startTime = Time.TotalSeconds.ToString(NumberFormatInfo.InvariantInfo);
var enableStart = EnableStart.TotalSeconds.ToString(NumberFormatInfo.InvariantInfo);
var enableFinish = EnableFinish.TotalSeconds.ToString(NumberFormatInfo.InvariantInfo);
return $"fade={InOut}:st={startTime}:d=1:alpha=1:enable='between(t,{enableStart},{enableFinish})'";
}
}
public record FadeInPoint(TimeSpan Time) : FadePoint(Time, "in");
public record FadeOutPoint(TimeSpan Time) : FadePoint(Time, "out");
+108 -68
View File
@@ -1,104 +1,144 @@
using System; using System;
using System.Globalization;
using System.IO;
using System.Text; using System.Text;
using ErsatzTV.Core.Interfaces.FFmpeg;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.FFmpeg namespace ErsatzTV.Core.FFmpeg
{ {
public class HlsPlaylistFilter public class HlsPlaylistFilter : IHlsPlaylistFilter
{ {
public static TrimPlaylistResult TrimPlaylist( private readonly ITempFilePool _tempFilePool;
private readonly ILogger<HlsPlaylistFilter> _logger;
public HlsPlaylistFilter(ITempFilePool tempFilePool, ILogger<HlsPlaylistFilter> logger)
{
_tempFilePool = tempFilePool;
_logger = logger;
}
public TrimPlaylistResult TrimPlaylist(
DateTimeOffset playlistStart, DateTimeOffset playlistStart,
DateTimeOffset filterBefore, DateTimeOffset filterBefore,
string[] lines, string[] lines,
int maxSegments = 10, int maxSegments = 10,
bool endWithDiscontinuity = false) bool endWithDiscontinuity = false)
{ {
DateTimeOffset currentTime = playlistStart; try
DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue;
var discontinuitySequence = 0;
var startSequence = 0;
var output = new StringBuilder();
var started = false;
var i = 0;
var segments = 0;
while (!lines[i].StartsWith("#EXTINF:"))
{ {
if (lines[i].StartsWith("#EXT-X-DISCONTINUITY-SEQUENCE")) DateTimeOffset currentTime = playlistStart;
{ DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue;
discontinuitySequence = int.Parse(lines[i].Split(':')[1]);
}
i++; var discontinuitySequence = 0;
} var startSequence = 0;
var output = new StringBuilder();
while (i < lines.Length) var started = false;
{ var i = 0;
if (segments >= maxSegments) var segments = 0;
while (!lines[i].StartsWith("#EXTINF:"))
{ {
break; if (lines[i].StartsWith("#EXT-X-DISCONTINUITY-SEQUENCE"))
}
string line = lines[i];
// _logger.LogInformation("Line: {Line}", line);
if (line.StartsWith("#EXT-X-DISCONTINUITY"))
{
if (started)
{ {
output.AppendLine("#EXT-X-DISCONTINUITY"); discontinuitySequence = int.Parse(lines[i].Split(':')[1]);
}
else
{
discontinuitySequence++;
} }
i++; i++;
continue;
} }
var duration = TimeSpan.FromSeconds(double.Parse(lines[i].TrimEnd(',').Split(':')[1])); while (i < lines.Length)
if (currentTime < filterBefore)
{ {
if (segments >= maxSegments)
{
break;
}
string line = lines[i];
// _logger.LogInformation("Line: {Line}", line);
if (line.StartsWith("#EXT-X-DISCONTINUITY"))
{
if (started)
{
output.AppendLine("#EXT-X-DISCONTINUITY");
}
else
{
discontinuitySequence++;
}
i++;
continue;
}
var duration = TimeSpan.FromSeconds(
double.Parse(
lines[i].TrimEnd(',').Split(':')[1],
NumberStyles.Number,
CultureInfo.InvariantCulture));
if (currentTime < filterBefore)
{
currentTime += duration;
i += 3;
continue;
}
nextPlaylistStart = currentTime < nextPlaylistStart ? currentTime : nextPlaylistStart;
if (!started)
{
startSequence = int.Parse(lines[i + 2].Replace("live", string.Empty).Split('.')[0]);
output.AppendLine("#EXTM3U");
output.AppendLine("#EXT-X-VERSION:6");
output.AppendLine("#EXT-X-TARGETDURATION:4");
output.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{startSequence}");
output.AppendLine($"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}");
output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
output.AppendLine("#EXT-X-DISCONTINUITY");
started = true;
}
output.AppendLine(lines[i]);
string offset = currentTime.ToString("zzz").Replace(":", string.Empty);
output.AppendLine($"#EXT-X-PROGRAM-DATE-TIME:{currentTime:yyyy-MM-ddTHH:mm:ss.fff}{offset}");
output.AppendLine(lines[i + 2]);
currentTime += duration; currentTime += duration;
segments++;
i += 3; i += 3;
continue;
} }
nextPlaylistStart = currentTime < nextPlaylistStart ? currentTime : nextPlaylistStart;
if (!started) var playlist = output.ToString();
if (endWithDiscontinuity && !playlist.EndsWith($"#EXT-X-DISCONTINUITY{Environment.NewLine}"))
{ {
startSequence = int.Parse(lines[i + 2].Replace("live", string.Empty).Split('.')[0]); playlist += "#EXT-X-DISCONTINUITY" + Environment.NewLine;
output.AppendLine("#EXTM3U");
output.AppendLine("#EXT-X-VERSION:6");
output.AppendLine("#EXT-X-TARGETDURATION:4");
output.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{startSequence}");
output.AppendLine($"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuitySequence}");
output.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
output.AppendLine("#EXT-X-DISCONTINUITY");
started = true;
} }
output.AppendLine(lines[i]); return new TrimPlaylistResult(nextPlaylistStart, startSequence, playlist, segments);
string offset = currentTime.ToString("zzz").Replace(":", string.Empty);
output.AppendLine($"#EXT-X-PROGRAM-DATE-TIME:{currentTime:yyyy-MM-ddTHH:mm:ss.fff}{offset}");
output.AppendLine(lines[i + 2]);
currentTime += duration;
segments++;
i += 3;
} }
catch (Exception ex)
if (endWithDiscontinuity)
{ {
output.AppendLine("#EXT-X-DISCONTINUITY"); try
} {
string file = _tempFilePool.GetNextTempFile(TempFileCategory.BadPlaylist);
File.WriteAllLines(file, lines);
return new TrimPlaylistResult(nextPlaylistStart, startSequence, output.ToString()); _logger.LogError(ex, "Error filtering playlist. Bad playlist saved to {BadPlaylistFile}", file);
// TODO: better error result?
return new TrimPlaylistResult(playlistStart, 0, string.Empty, 0);
}
catch
{
// do nothing
}
throw;
}
} }
public static TrimPlaylistResult TrimPlaylistWithDiscontinuity( public TrimPlaylistResult TrimPlaylistWithDiscontinuity(
DateTimeOffset playlistStart, DateTimeOffset playlistStart,
DateTimeOffset filterBefore, DateTimeOffset filterBefore,
string[] lines) string[] lines)
@@ -107,5 +147,5 @@ namespace ErsatzTV.Core.FFmpeg
} }
} }
public record TrimPlaylistResult(DateTimeOffset PlaylistStart, int Sequence, string Playlist); public record TrimPlaylistResult(DateTimeOffset PlaylistStart, int Sequence, string Playlist, int SegmentCount);
} }
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.FFmpeg;
namespace ErsatzTV.Core.FFmpeg;
public interface IFFmpegProcessServiceFactory
{
Task<IFFmpegProcessService> GetService();
}
@@ -0,0 +1,18 @@
using System;
namespace ErsatzTV.Core.FFmpeg;
public interface IHlsPlaylistFilter
{
TrimPlaylistResult TrimPlaylist(
DateTimeOffset playlistStart,
DateTimeOffset filterBefore,
string[] lines,
int maxSegments = 10,
bool endWithDiscontinuity = false);
TrimPlaylistResult TrimPlaylistWithDiscontinuity(
DateTimeOffset playlistStart,
DateTimeOffset filterBefore,
string[] lines);
}
+20 -11
View File
@@ -7,6 +7,8 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.State;
using LanguageExt; using LanguageExt;
using static LanguageExt.Prelude; using static LanguageExt.Prelude;
@@ -19,16 +21,16 @@ namespace ErsatzTV.Core.FFmpeg
private readonly ITempFilePool _tempFilePool; private readonly ITempFilePool _tempFilePool;
private readonly IImageCache _imageCache; private readonly IImageCache _imageCache;
private readonly IFFmpegProcessService _ffmpegProcessService; private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
public SongVideoGenerator( public SongVideoGenerator(
ITempFilePool tempFilePool, ITempFilePool tempFilePool,
IImageCache imageCache, IImageCache imageCache,
IFFmpegProcessService ffmpegProcessService) IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
{ {
_tempFilePool = tempFilePool; _tempFilePool = tempFilePool;
_imageCache = imageCache; _imageCache = imageCache;
_ffmpegProcessService = ffmpegProcessService; _ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
} }
public async Task<Tuple<string, MediaVersion>> GenerateSongVideo( public async Task<Tuple<string, MediaVersion>> GenerateSongVideo(
@@ -48,7 +50,7 @@ namespace ErsatzTV.Core.FFmpeg
SampleAspectRatio = "1:1", SampleAspectRatio = "1:1",
Streams = new List<MediaStream> Streams = new List<MediaStream>
{ {
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 } new() { MediaStreamKind = MediaStreamKind.Video, Index = 0, PixelFormat = "yuv420p" }
} }
}; };
@@ -71,9 +73,9 @@ namespace ErsatzTV.Core.FFmpeg
const int HORIZONTAL_MARGIN_PERCENT = 3; const int HORIZONTAL_MARGIN_PERCENT = 3;
const int VERTICAL_MARGIN_PERCENT = 5; const int VERTICAL_MARGIN_PERCENT = 5;
const int WATERMARK_WIDTH_PERCENT = 25; const int WATERMARK_WIDTH_PERCENT = 25;
ChannelWatermarkLocation watermarkLocation = NextRandom(2) == 0 WatermarkLocation watermarkLocation = NextRandom(2) == 0
? ChannelWatermarkLocation.BottomLeft ? WatermarkLocation.BottomLeft
: ChannelWatermarkLocation.BottomRight; : WatermarkLocation.BottomRight;
foreach (SongMetadata metadata in song.SongMetadata) foreach (SongMetadata metadata in song.SongMetadata)
{ {
@@ -120,10 +122,10 @@ namespace ErsatzTV.Core.FFmpeg
{ {
switch (watermarkLocation) switch (watermarkLocation)
{ {
case ChannelWatermarkLocation.BottomLeft: case WatermarkLocation.BottomLeft:
leftMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT; leftMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
break; break;
case ChannelWatermarkLocation.BottomRight: case WatermarkLocation.BottomRight:
leftMarginPercent = rightMarginPercent = HORIZONTAL_MARGIN_PERCENT; leftMarginPercent = rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
rightMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT; rightMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
break; break;
@@ -208,7 +210,8 @@ namespace ErsatzTV.Core.FFmpeg
new() { Path = videoPath } new() { Path = videoPath }
}; };
Either<BaseError, string> maybeSongImage = await _ffmpegProcessService.GenerateSongImage( IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
Either<BaseError, string> maybeSongImage = await ffmpegProcessService.GenerateSongImage(
ffmpegPath, ffmpegPath,
subtitleFile, subtitleFile,
channel, channel,
@@ -234,7 +237,13 @@ namespace ErsatzTV.Core.FFmpeg
SampleAspectRatio = "1:1", SampleAspectRatio = "1:1",
Streams = new List<MediaStream> Streams = new List<MediaStream>
{ {
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }, new()
{
MediaStreamKind = MediaStreamKind.Video,
Index = 0,
Codec = VideoFormat.GeneratedImage,
PixelFormat = new PixelFormatUnknown().Name // the resulting pixel format is unknown
},
}, },
MediaFiles = new List<MediaFile> MediaFiles = new List<MediaFile>
{ {
+3 -1
View File
@@ -5,6 +5,8 @@
Subtitle = 0, Subtitle = 0,
SongBackground = 1, SongBackground = 1,
CoverArt = 2, CoverArt = 2,
CachedArtwork = 3 CachedArtwork = 3,
BadPlaylist = 99
} }
} }
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LanguageExt;
namespace ErsatzTV.Core.FFmpeg;
public static class WatermarkCalculator
{
public static List<FadePoint> CalculateFadePoints(
DateTimeOffset itemStartTime,
TimeSpan inPoint,
TimeSpan outPoint,
Option<TimeSpan> streamSeek,
int frequencyMinutes,
int durationSeconds)
{
var result = new List<FadePoint>();
TimeSpan duration = outPoint - inPoint;
DateTimeOffset itemFinishTime = itemStartTime + duration;
DateTimeOffset start = itemStartTime.AddMinutes(-16);
// find the next whole minute
if (start.Second > 0 || start.Millisecond > 0)
{
start = start.AddMinutes(1);
start = start.AddSeconds(-start.Second);
start = start.AddMilliseconds(-start.Millisecond);
}
DateTimeOffset finish = itemFinishTime;
// find the previous whole minute
if (finish.Second > 0 || finish.Millisecond > 0)
{
finish = finish.AddSeconds(-finish.Second);
finish = finish.AddMilliseconds(-finish.Millisecond);
}
DateTimeOffset current = start;
while (current <= finish)
{
current = current.AddMinutes(1);
if (current.Minute % frequencyMinutes == 0)
{
TimeSpan fadeInTime = inPoint + (current - itemStartTime);
result.Add(new FadeInPoint(fadeInTime));
result.Add(new FadeOutPoint(fadeInTime.Add(TimeSpan.FromSeconds(durationSeconds))));
}
}
// if we're seeking, subtract the seek from each item and return that
foreach (TimeSpan ss in streamSeek)
{
result = result.Map(fp => fp with { Time = fp.Time - ss }).ToList();
}
// trim points that have already passed
result.RemoveAll(fp => fp.Time < TimeSpan.Zero);
// trim points that are past the end
result.RemoveAll(fp => fp.Time >= outPoint);
if (result.Any())
{
for (var i = 0; i < result.Count; i++)
{
result[i].EnableStart = i == 0 ? TimeSpan.Zero : result[i - 1].Time.Add(TimeSpan.FromSeconds(1));
}
for (var i = 0; i < result.Count; i++)
{
result[i].EnableFinish = i == result.Count - 1
? outPoint
: result[i + 1].Time.Subtract(TimeSpan.FromSeconds(1));
}
}
return result;
}
}
+3
View File
@@ -18,9 +18,12 @@ namespace ErsatzTV.Core
Environment.SpecialFolderOption.Create), Environment.SpecialFolderOption.Create),
"etv-transcode"); "etv-transcode");
public static readonly string LogsFolder = Path.Combine(AppDataFolder, "logs");
public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3"); public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3"); public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
public static readonly string LogFilePath = Path.Combine(LogsFolder, "ersatztv.log");
public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images"); public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources"); public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources");

Some files were not shown because too many files have changed in this diff Show More