Compare commits
99
Commits
develop
...
v0.4.2-alpha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3ef90880 | ||
|
|
696b29c9e9 | ||
|
|
70c37df596 | ||
|
|
040785b0d7 | ||
|
|
b25f783343 | ||
|
|
a21f62ff8c | ||
|
|
78fdc9c57a | ||
|
|
f6c42f3ff5 | ||
|
|
c92b6cb909 | ||
|
|
a2e1dc8bfb | ||
|
|
8a6093ce8d | ||
|
|
1d6279cee8 | ||
|
|
66ab0b3990 | ||
|
|
a7922beaed | ||
|
|
a1d9d6790e | ||
|
|
2f2d7952dd | ||
|
|
c96b800b52 | ||
|
|
c05882f4a6 | ||
|
|
5a442a06a0 | ||
|
|
640fed0a43 | ||
|
|
ab1f294c1f | ||
|
|
ea08453913 | ||
|
|
87deaa6f3a | ||
|
|
9d99c19ea4 | ||
|
|
49d14b05f6 | ||
|
|
a8ba9edf2b | ||
|
|
89811a1203 | ||
|
|
534e2c4512 | ||
|
|
c1e148633d | ||
|
|
a9dff5eff7 | ||
|
|
a2da043f4b | ||
|
|
252c185562 | ||
|
|
a47987a9d7 | ||
|
|
5937211bb8 | ||
|
|
e32dbd0474 | ||
|
|
6bcc1ede2b | ||
|
|
6c9764a51e | ||
|
|
ff5438459c | ||
|
|
0c53a4509c | ||
|
|
5fd315ead8 | ||
|
|
f02b0ac345 | ||
|
|
fd83007296 | ||
|
|
70ca5bf050 | ||
|
|
eed9f60273 | ||
|
|
0e2e6cd52e | ||
|
|
c9b557f2e6 | ||
|
|
cde869f3eb | ||
|
|
90d6a59d3f | ||
|
|
b972947747 | ||
|
|
17bc988b49 | ||
|
|
749eea836b | ||
|
|
37c52c4cb4 | ||
|
|
33ba58aa68 | ||
|
|
5f6043e593 | ||
|
|
96e95a21fb | ||
|
|
9168fd6bf2 | ||
|
|
14413f62a7 | ||
|
|
34c71a0c12 | ||
|
|
a487e7fe15 | ||
|
|
cd4ea42597 | ||
|
|
a3d42145f7 | ||
|
|
261cf5052a | ||
|
|
de9af2f0f6 | ||
|
|
8d4e18ed2f | ||
|
|
1ee01c1d78 | ||
|
|
7de50dd916 | ||
|
|
744fd3beaa | ||
|
|
861c95e1bd | ||
|
|
bb5b9f9be4 | ||
|
|
135628441a | ||
|
|
4aa7204984 | ||
|
|
1af59a0337 | ||
|
|
c4c97fcc8c | ||
|
|
9c46e42792 | ||
|
|
efa803aab6 | ||
|
|
6ea02a2d77 | ||
|
|
631f7d2d5e | ||
|
|
e44a4cb2e1 | ||
|
|
f4b95419a6 | ||
|
|
1a5cf49563 | ||
|
|
efef0b0fee | ||
|
|
ee7b8a71ab | ||
|
|
e7c9a51e96 | ||
|
|
78a954f365 | ||
|
|
355c0b7be9 | ||
|
|
3bcb2d36f9 | ||
|
|
b240de9d4a | ||
|
|
f5001837cb | ||
|
|
6ea916b1f0 | ||
|
|
db6fd22215 | ||
|
|
691842008d | ||
|
|
685f78bef8 | ||
|
|
3ce267863b | ||
|
|
e4231cb57d | ||
|
|
03946b13ca | ||
|
|
f1a81bf086 | ||
|
|
7a88374362 | ||
|
|
663a62431b | ||
|
|
1d4acc284d |
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -3,7 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Deploy docs
|
name: Deploy docs
|
||||||
|
|||||||
@@ -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
@@ -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
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "ErsatzTV-macOS"]
|
||||||
|
path = ErsatzTV-macOS
|
||||||
|
url = git@github.com:jasongdove/ErsatzTV-macOS.git
|
||||||
+83
-2
@@ -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
|
||||||
|
|||||||
@@ -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 |
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule
+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>();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+1
-1
@@ -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>>;
|
||||||
}
|
}
|
||||||
+7
-4
@@ -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) =>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -66,6 +71,13 @@ namespace ErsatzTV.Application.Streaming
|
|||||||
|
|
||||||
_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;
|
||||||
_playlistStart = _transcodedUntil;
|
_playlistStart = _transcodedUntil;
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-11
@@ -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)
|
||||||
@@ -291,6 +306,213 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -18,8 +19,31 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator();
|
public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Should_UseSpecifiedThreadCount_ForTransportStream()
|
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]
|
||||||
|
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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
var args =
|
string videoFilter = videoScanKind == VideoScanKind.Interlaced ? "-vf tinterlace=interleave_top,fieldorder=tff" : string.Empty;
|
||||||
$"-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}";
|
string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty;
|
||||||
|
|
||||||
|
string args =
|
||||||
|
$"-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,20 +289,17 @@ 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",
|
||||||
@@ -235,7 +307,38 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
"Provided device doesn't support"
|
"Provided device doesn't support"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (profileAcceleration != HardwareAccelerationKind.None && unsupportedMessages.Any(error.Contains))
|
var errorBuffer = new StringBuilder();
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -94,19 +100,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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();
|
||||||
@@ -159,19 +171,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.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();
|
||||||
@@ -221,47 +239,53 @@ 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));
|
||||||
|
|
||||||
// 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();
|
||||||
@@ -291,6 +315,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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();
|
||||||
@@ -371,6 +401,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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();
|
||||||
@@ -463,6 +499,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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();
|
||||||
@@ -562,6 +604,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -41,15 +41,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, 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));
|
||||||
@@ -110,15 +116,21 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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, 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);
|
||||||
}
|
}
|
||||||
@@ -269,15 +287,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, 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);
|
||||||
}
|
}
|
||||||
@@ -344,15 +368,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.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);
|
||||||
}
|
}
|
||||||
@@ -435,15 +465,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.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);
|
||||||
}
|
}
|
||||||
@@ -516,15 +552,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.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);
|
||||||
}
|
}
|
||||||
@@ -617,9 +659,15 @@ 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(
|
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);
|
||||||
}
|
}
|
||||||
@@ -730,9 +778,15 @@ 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(
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -43,15 +47,17 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -99,6 +105,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -108,15 +118,17 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -170,6 +182,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -184,15 +200,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
{ 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);
|
||||||
}
|
}
|
||||||
@@ -262,6 +280,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -276,15 +298,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
{ 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);
|
||||||
}
|
}
|
||||||
@@ -344,6 +368,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -358,15 +386,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
{ 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);
|
||||||
}
|
}
|
||||||
@@ -442,6 +472,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -557,6 +593,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -576,9 +616,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,
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,19 +31,25 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -92,6 +98,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -157,6 +169,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -165,15 +181,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -232,6 +250,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -240,15 +262,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -297,6 +321,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,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -305,15 +333,17 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
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);
|
||||||
}
|
}
|
||||||
@@ -378,6 +408,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -390,9 +424,11 @@ 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(
|
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);
|
||||||
}
|
}
|
||||||
@@ -475,6 +511,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -487,9 +527,11 @@ 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(
|
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);
|
||||||
}
|
}
|
||||||
@@ -554,6 +596,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -641,6 +689,10 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var scheduleItemsEnumerator = new OrderedScheduleItemsEnumerator(
|
||||||
|
new List<ProgramScheduleItem> { scheduleItem },
|
||||||
|
new CollectionEnumeratorState());
|
||||||
|
|
||||||
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
var enumerator1 = new ChronologicalMediaCollectionEnumerator(
|
||||||
collectionOne.MediaItems,
|
collectionOne.MediaItems,
|
||||||
new CollectionEnumeratorState());
|
new CollectionEnumeratorState());
|
||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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")
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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">
|
||||||
@@ -26,4 +27,8 @@
|
|||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ErsatzTV.FFmpeg\ErsatzTV.FFmpeg.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,94 +428,46 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,27 +214,59 @@ 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -380,35 +423,32 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
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,15 +643,15 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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;
|
var playlist = output.ToString();
|
||||||
|
if (endWithDiscontinuity && !playlist.EndsWith($"#EXT-X-DISCONTINUITY{Environment.NewLine}"))
|
||||||
if (!started)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user