From 218015063b18aaaae2de51f2e77cf451757dc874 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Mon, 7 Oct 2019 22:29:31 -0400 Subject: [PATCH 01/60] Port former create_tarball into COPR Makefile This script was removed in #1793; instead of restoring it, instead implement its functionality directly in the COPR Makefile. --- .copr/Makefile | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.copr/Makefile b/.copr/Makefile index 84b98a0116..6c1f9a3c43 100644 --- a/.copr/Makefile +++ b/.copr/Makefile @@ -2,7 +2,49 @@ srpm: dnf -y install git git submodule update --init --recursive cd deployment/fedora-package-x64; \ - ./create_tarball.sh; \ + WORKDIR="$( pwd )"; \ + VERSION="$( sed -ne '/^Version:/s/.* *//p' "${WORKDIR}"/pkg-src/jellyfin.spec )"; \ + package_temporary_dir="${WORKDIR}/pkg-dist-tmp"; \ + pkg_src_dir="${WORKDIR}/pkg-src"; \ + GNU_TAR=1; \ + tar \ + --transform "s,^\.,jellyfin-${VERSION}," \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \ + -C ${SOURCE_DIR} ./ || GNU_TAR=0; \ + if [ $GNU_TAR -eq 0 ]; then + package_temporary_dir="$( mktemp -d )"; \ + mkdir -p "${package_temporary_dir}/jellyfin"; \ + tar \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ + -C ${SOURCE_DIR} ./; \ + mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}"; \ + tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"; \ + rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"; \ + tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"; \ + rm -rf ${package_temporary_dir}; \ + fi; \ rpmbuild -bs pkg-src/jellyfin.spec \ --define "_sourcedir $$PWD/pkg-src/" \ --define "_srcrpmdir $(outdir)" From c8ffa2fb1d2c56306fa9bca19d73c9842ba5b0b4 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Sat, 12 Oct 2019 03:03:33 -0400 Subject: [PATCH 02/60] Set service install default to no --- deployment/windows/jellyfin.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi index e33efde916..8b6c1e01e7 100644 --- a/deployment/windows/jellyfin.nsi +++ b/deployment/windows/jellyfin.nsi @@ -321,7 +321,7 @@ SectionEnd Function .onInit ; Setting up defaults - StrCpy $_INSTALLSERVICE_ "Yes" + StrCpy $_INSTALLSERVICE_ "No" StrCpy $_SERVICESTART_ "Yes" StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" StrCpy $_EXISTINGINSTALLATION_ "No" From 04c4ad731e36fc36d5e924cc7adf0e12f25cf1b5 Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Sat, 12 Oct 2019 03:56:46 -0400 Subject: [PATCH 03/60] Begin to add setup type dialog --- deployment/windows/dialogs/setuptype.nsddef | 12 +++++ deployment/windows/dialogs/setuptype.nsdinc | 56 +++++++++++++++++++++ deployment/windows/jellyfin.nsi | 20 +++++++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 deployment/windows/dialogs/setuptype.nsddef create mode 100644 deployment/windows/dialogs/setuptype.nsdinc diff --git a/deployment/windows/dialogs/setuptype.nsddef b/deployment/windows/dialogs/setuptype.nsddef new file mode 100644 index 0000000000..ff59f62152 --- /dev/null +++ b/deployment/windows/dialogs/setuptype.nsddef @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/deployment/windows/dialogs/setuptype.nsdinc b/deployment/windows/dialogs/setuptype.nsdinc new file mode 100644 index 0000000000..0b171da282 --- /dev/null +++ b/deployment/windows/dialogs/setuptype.nsdinc @@ -0,0 +1,56 @@ +; ========================================================= +; This file was generated by NSISDialogDesigner 1.4.4.0 +; http://coolsoft.altervista.org/nsisdialogdesigner +; +; Do not edit it manually, use NSISDialogDesigner instead! +; ========================================================= + +; handle variables +Var hCtl_setuptype +Var hCtl_setuptype_InstallasaServiceLabel +Var hCtl_setuptype_InstallasaService +Var hCtl_setuptype_BasicInstallLabel +Var hCtl_setuptype_BasicInstall +Var hCtl_setuptype_Font1 + + +; dialog create function +Function fnc_setuptype_Create + + ; custom font definitions + CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700" + + ; === setuptype (type: Dialog) === + nsDialogs::Create 1018 + Pop $hCtl_setuptype + ${If} $hCtl_setuptype == error + Abort + ${EndIf} + !insertmacro MUI_HEADER_TEXT "This controls how Jellyfin is installed." "Setup Type" + + ; === InstallasaServiceLabel (type: Label) === + ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." + Pop $hCtl_setuptype_InstallasaServiceLabel + + ; === InstallasaService (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)" + Pop $hCtl_setuptype_InstallasaService + ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP} + + ; === BasicInstallLabel (type: Label) === + ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." + Pop $hCtl_setuptype_BasicInstallLabel + + ; === BasicInstall (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)" + Pop $hCtl_setuptype_BasicInstall + SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0 + ${NSD_Check} $hCtl_setuptype_BasicInstall + +FunctionEnd + +; dialog show function +Function fnc_setuptype_Show + Call fnc_setuptype_Create + nsDialogs::Show +FunctionEnd diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi index 8b6c1e01e7..847666554e 100644 --- a/deployment/windows/jellyfin.nsi +++ b/deployment/windows/jellyfin.nsi @@ -16,6 +16,7 @@ ShowUninstDetails show ; Global variables that we'll use Var _JELLYFINVERSION_ Var _JELLYFINDATADIR_ + Var _SETUPTYPE_ Var _INSTALLSERVICE_ Var _SERVICESTART_ Var _SERVICEACCOUNTTYPE_ @@ -86,6 +87,9 @@ ShowUninstDetails show !insertmacro MUI_PAGE_WELCOME ; License Page !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL + +; Setup Type Page + Page custom ShowSetupTypePage SetupTypePage_Config ; Components Page !insertmacro MUI_PAGE_COMPONENTS !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show @@ -102,6 +106,7 @@ ShowUninstDetails show !insertmacro MUI_PAGE_DIRECTORY ; Custom Dialogs + !include "dialogs\setuptype.nsdinc" !include "dialogs\service-config.nsdinc" !include "dialogs\confirmation.nsdinc" @@ -170,7 +175,7 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"' WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0' WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project" - WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.media/" + WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/" WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_" WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1 WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1 @@ -413,6 +418,19 @@ Function HideConfirmationPage ${EndIf} FunctionEnd +Function HideSetupTypePage + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder + Abort + ${EndIf} +FunctionEnd + +; Setup Type dialog show function +Function ShowSetupTypePage + Call HideSetupTypePage + Call fnc_setuptype_Create + nsDialogs::Show +FunctionEnd + ; Service Config dialog show function Function ShowServiceConfigPage Call HideServiceConfigPage From 05fb84ba2236b0fd0727caf29fad4986059c8b8e Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Sat, 12 Oct 2019 04:39:34 -0400 Subject: [PATCH 04/60] Make service setup an optional component, remove setuptype page config for now --- deployment/windows/jellyfin.nsi | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi index 847666554e..b8de4a4401 100644 --- a/deployment/windows/jellyfin.nsi +++ b/deployment/windows/jellyfin.nsi @@ -89,7 +89,8 @@ ShowUninstDetails show !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL ; Setup Type Page - Page custom ShowSetupTypePage SetupTypePage_Config + Page custom ShowSetupTypePage ;SetupTypePage_Config + ; Components Page !insertmacro MUI_PAGE_COMPONENTS !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show @@ -184,7 +185,7 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer WriteUninstaller "$INSTDIR\Uninstall.exe" SectionEnd -Section "Jellyfin Server Service" InstallService +Section /o "Jellyfin Server Service" InstallService ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 DetailPrint "Jellyfin Server service statuscode, $0" From 6746f708f2c2f306ea04270b93cfed2e28eb36c9 Mon Sep 17 00:00:00 2001 From: Vasily Date: Thu, 3 Oct 2019 16:17:35 +0300 Subject: [PATCH 05/60] Revert "Revert "Fix premature stop when streaming"" This reverts commit 575b96d03a2cf98a0e5dd9d9f7329adca34c0311. --- .../Playback/Hls/DynamicHlsService.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index f5f7536843..8fa6c3dac0 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -965,14 +965,6 @@ namespace MediaBrowser.Api.Playback.Hls var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request); - var timeDeltaParam = string.Empty; - - if (isEncoding && state.TargetFramerate > 0) - { - float startTime = 1 / (state.TargetFramerate.Value * 2); - timeDeltaParam = string.Format(CultureInfo.InvariantCulture, "-segment_time_delta {0:F3}", startTime); - } - var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.'); if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) { @@ -980,7 +972,7 @@ namespace MediaBrowser.Api.Playback.Hls } return string.Format( - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f hls -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", inputModifier, EncodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -988,11 +980,10 @@ namespace MediaBrowser.Api.Playback.Hls GetVideoArguments(state, encodingOptions), GetAudioArguments(state, encodingOptions), state.SegmentLength.ToString(CultureInfo.InvariantCulture), + segmentFormat, startNumberParam, - outputPath, outputTsArg, - timeDeltaParam, - segmentFormat + outputPath ).Trim(); } } From c1f9107b8be9b3cbd26e15773998c7ac6598e7f9 Mon Sep 17 00:00:00 2001 From: Vasily Date: Thu, 3 Oct 2019 18:50:39 +0300 Subject: [PATCH 06/60] Add more logging Trying to fix hls muxer plus ffmpeg 4.1+ combo Try to fix waiting for segment being ready This is needed because hls muxer in ffmpeg >= 4.1 creates the playlist only when it finishes transcoding. Also cleaned up logs a bit. Lower log level for "StartFfmpeg finished" to debug --- .../Playback/BaseStreamingService.cs | 7 ++- .../Playback/Hls/DynamicHlsService.cs | 47 ++++++++----------- deployment/debian-package-x64/docker-build.sh | 1 + 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 8c4ccfa22c..700883c963 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -289,16 +289,20 @@ namespace MediaBrowser.Api.Playback throw; } + Logger.LogDebug("Launched ffmpeg process"); state.TranscodingJob = transcodingJob; // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); // Wait for the file to exist before proceeeding - while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) + var waitFor = state.WaitForPath ?? outputPath; + Logger.LogDebug("Waiting for the creation of '{0}'", waitFor); + while (!File.Exists(waitFor) && !transcodingJob.HasExited) { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } + Logger.LogDebug("File '{0}' created or transcoding has finished", waitFor); if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited) { @@ -314,6 +318,7 @@ namespace MediaBrowser.Api.Playback { StartThrottler(state, transcodingJob); } + Logger.LogDebug("StartFfMpeg() finished successfully"); return transcodingJob; } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 8fa6c3dac0..15dd5add4a 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -243,6 +243,7 @@ namespace MediaBrowser.Api.Playback.Hls request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex); + state.WaitForPath = segmentPath; job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); } catch @@ -458,16 +459,15 @@ namespace MediaBrowser.Api.Playback.Hls TranscodingJob transcodingJob, CancellationToken cancellationToken) { - var segmentFileExists = File.Exists(segmentPath); - - // If all transcoding has completed, just return immediately - if (transcodingJob != null && transcodingJob.HasExited && segmentFileExists) + var segmentExists = File.Exists(segmentPath); + if (segmentExists) { - return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); - } + if (transcodingJob != null && transcodingJob.HasExited) + { + // Transcoding job is over, so assume all existing files are ready + return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); + } - if (segmentFileExists) - { var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready @@ -477,33 +477,26 @@ namespace MediaBrowser.Api.Playback.Hls } } - var segmentFilename = Path.GetFileName(segmentPath); - + var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1); while (!cancellationToken.IsCancellationRequested) { - try + // To be considered ready, the segment file has to exist AND + // either the transcoding job should be done or next segment should also exit + if (segmentExists) { - var text = File.ReadAllText(playlistPath, Encoding.UTF8); - - // If it appears in the playlist, it's done - if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + if ((transcodingJob != null && transcodingJob.HasExited) || File.Exists(nextSegmentPath)) { - if (!segmentFileExists) - { - segmentFileExists = File.Exists(segmentPath); - } - if (segmentFileExists) - { - return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); - } - //break; + return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } } - catch (IOException) + else { - // May get an error if the file is locked + segmentExists = File.Exists(segmentPath); + if (segmentExists) + { + continue; // avoid unnecessary waiting if segment just became available + } } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); } diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh index 97bc45a060..5911923d78 100755 --- a/deployment/debian-package-x64/docker-build.sh +++ b/deployment/debian-package-x64/docker-build.sh @@ -20,6 +20,7 @@ if [[ -n ${web_branch} ]]; then checkout -b origin/${web_branch} fi yarn install +yarn build mkdir -p ${web_target} mv dist/* ${web_target}/ popd From 7aea9266d05ddd0673c390e102833c154b3ff9f6 Mon Sep 17 00:00:00 2001 From: Vasily Date: Tue, 8 Oct 2019 16:34:43 +0300 Subject: [PATCH 07/60] Stop waiting for a segment to become ready if there's no alive transcode Remove extra quotes in logging Fix typo in comment --- .../Playback/BaseStreamingService.cs | 4 ++-- .../Playback/Hls/DynamicHlsService.cs | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 700883c963..2e9c1c3d4c 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -297,12 +297,12 @@ namespace MediaBrowser.Api.Playback // Wait for the file to exist before proceeeding var waitFor = state.WaitForPath ?? outputPath; - Logger.LogDebug("Waiting for the creation of '{0}'", waitFor); + Logger.LogDebug("Waiting for the creation of {0}", waitFor); while (!File.Exists(waitFor) && !transcodingJob.HasExited) { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } - Logger.LogDebug("File '{0}' created or transcoding has finished", waitFor); + Logger.LogDebug("File {0} created or transcoding has finished", waitFor); if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited) { diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 15dd5add4a..a06a2f84d9 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -192,6 +192,7 @@ namespace MediaBrowser.Api.Playback.Hls if (File.Exists(segmentPath)) { job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -207,6 +208,7 @@ namespace MediaBrowser.Api.Playback.Hls job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); transcodingLock.Release(); released = true; + Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } else @@ -278,7 +280,7 @@ namespace MediaBrowser.Api.Playback.Hls // await Task.Delay(50, cancellationToken).ConfigureAwait(false); //} - Logger.LogInformation("returning {0}", segmentPath); + Logger.LogDebug("returning {0} [general case]", segmentPath); job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -465,6 +467,7 @@ namespace MediaBrowser.Api.Playback.Hls if (transcodingJob != null && transcodingJob.HasExited) { // Transcoding job is over, so assume all existing files are ready + Logger.LogDebug("serving up {0} as transcode is over", segmentPath); return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } @@ -473,19 +476,21 @@ namespace MediaBrowser.Api.Playback.Hls // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready if (segmentIndex < currentTranscodingIndex) { + Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, segmentIndex, currentTranscodingIndex); return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } } var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1); - while (!cancellationToken.IsCancellationRequested) + while (!cancellationToken.IsCancellationRequested && transcodingJob != null && !transcodingJob.HasExited) { // To be considered ready, the segment file has to exist AND - // either the transcoding job should be done or next segment should also exit + // either the transcoding job should be done or next segment should also exist if (segmentExists) { - if ((transcodingJob != null && transcodingJob.HasExited) || File.Exists(nextSegmentPath)) + if (transcodingJob.HasExited || File.Exists(nextSegmentPath)) { + Logger.LogDebug("serving up {0} as it deemed ready", segmentPath); return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } } @@ -501,6 +506,14 @@ namespace MediaBrowser.Api.Playback.Hls } cancellationToken.ThrowIfCancellationRequested(); + if (!File.Exists(segmentPath)) + { + Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath); + } + else + { + Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath); + } return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } @@ -514,6 +527,7 @@ namespace MediaBrowser.Api.Playback.Hls FileShare = FileShareMode.ReadWrite, OnComplete = () => { + Logger.LogDebug("finished serving {0}", segmentPath); if (transcodingJob != null) { transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); From 3740228100b15c9feae1864f5652674af69e582c Mon Sep 17 00:00:00 2001 From: Vasily Date: Tue, 8 Oct 2019 17:00:16 +0300 Subject: [PATCH 08/60] Don't start waiting for a segment which doesn't exist if transcoding is not running --- .../Playback/Hls/DynamicHlsService.cs | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index a06a2f84d9..43cf924274 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -482,38 +482,46 @@ namespace MediaBrowser.Api.Playback.Hls } var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1); - while (!cancellationToken.IsCancellationRequested && transcodingJob != null && !transcodingJob.HasExited) + if (transcodingJob != null) { - // To be considered ready, the segment file has to exist AND - // either the transcoding job should be done or next segment should also exist - if (segmentExists) + while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited) { - if (transcodingJob.HasExited || File.Exists(nextSegmentPath)) + // To be considered ready, the segment file has to exist AND + // either the transcoding job should be done or next segment should also exist + if (segmentExists) { - Logger.LogDebug("serving up {0} as it deemed ready", segmentPath); - return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); + if (transcodingJob.HasExited || File.Exists(nextSegmentPath)) + { + Logger.LogDebug("serving up {0} as it deemed ready", segmentPath); + return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); + } } + else + { + segmentExists = File.Exists(segmentPath); + if (segmentExists) + { + continue; // avoid unnecessary waiting if segment just became available + } + } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + if (!File.Exists(segmentPath)) + { + Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath); } else { - segmentExists = File.Exists(segmentPath); - if (segmentExists) - { - continue; // avoid unnecessary waiting if segment just became available - } + Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath); } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - - cancellationToken.ThrowIfCancellationRequested(); - if (!File.Exists(segmentPath)) - { - Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath); } else { - Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath); + Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath); } + return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } From 986ea5c636ead098446cca31ea737d2f361a05d8 Mon Sep 17 00:00:00 2001 From: Vasily Date: Wed, 9 Oct 2019 12:55:09 +0300 Subject: [PATCH 09/60] Fix log message - log args were swapped --- MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 43cf924274..069dd41d02 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -476,7 +476,7 @@ namespace MediaBrowser.Api.Playback.Hls // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready if (segmentIndex < currentTranscodingIndex) { - Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, segmentIndex, currentTranscodingIndex); + Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex); return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } } From 2f6879e8699e53c3d140dacd9af8c207bdf8cb74 Mon Sep 17 00:00:00 2001 From: Vasily Date: Wed, 9 Oct 2019 13:46:01 +0300 Subject: [PATCH 10/60] Add limiting max keyframe interval when full transcoding --- MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 069dd41d02..8cbe4af1e8 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -927,6 +927,18 @@ namespace MediaBrowser.Api.Playback.Hls " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", GetStartNumber(state) * state.SegmentLength, state.SegmentLength.ToString(CultureInfo.InvariantCulture)); + if (state.TargetFramerate.HasValue) + { + // This is to make sure keyframe interval is limited to our segment, + // as forcing keyframes is not enough. + // Example: we ecoded half of desired length, then codec detected + // scene cut and inserted a keyframe; next forced keyframe would + // be created outside of segment, which breaks seeking. + keyFrameArg += string.Format( + " -g {0} -keyint_min {0}", + (int)(state.SegmentLength * state.TargetFramerate) + ); + } var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; From 82f8345aa5ae65a8a934a4f2d29afdbc3836d875 Mon Sep 17 00:00:00 2001 From: Vasily Date: Thu, 10 Oct 2019 18:47:02 +0300 Subject: [PATCH 11/60] Log to debug all HTTP 500 response urls --- Emby.Server.Implementations/HttpServer/HttpListenerHost.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e4f98acb9b..0ebdf7d178 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -539,6 +539,10 @@ namespace Emby.Server.Implementations.HttpServer } finally { + if (httpRes.StatusCode >= 500) + { + _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog); + } stopWatch.Stop(); var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) From 1bd12083c3d7eda02bd7e3b785f4853620dc6cc9 Mon Sep 17 00:00:00 2001 From: Vasily Date: Mon, 14 Oct 2019 13:12:10 +0300 Subject: [PATCH 12/60] Respect non-inversed setting of "enable break on non-keyframes" --- MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 991fc0b001..0aa0679045 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2168,7 +2168,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) && state.TranscodingType != TranscodingJobType.Progressive - && state.EnableBreakOnNonKeyFrames(outputVideoCodec)) + && !state.EnableBreakOnNonKeyFrames(outputVideoCodec) && + (state.BaseRequest.StartTimeTicks ?? 0) > 0) { inputModifier += " -noaccurate_seek"; } From 3132280b0748efd6ded7d8c9d8795506ef8c30ed Mon Sep 17 00:00:00 2001 From: Vasily Date: Mon, 14 Oct 2019 13:13:04 +0300 Subject: [PATCH 13/60] * Make sure force_key_frames expression arguments are properly converted to strings * Fore usage of keyframe cuts only in HLS --- MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 8cbe4af1e8..1b93eaaa57 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -925,7 +925,7 @@ namespace MediaBrowser.Api.Playback.Hls { var keyFrameArg = string.Format( " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", - GetStartNumber(state) * state.SegmentLength, + (GetStartNumber(state) * state.SegmentLength).ToString(CultureInfo.InvariantCulture), state.SegmentLength.ToString(CultureInfo.InvariantCulture)); if (state.TargetFramerate.HasValue) { @@ -936,7 +936,7 @@ namespace MediaBrowser.Api.Playback.Hls // be created outside of segment, which breaks seeking. keyFrameArg += string.Format( " -g {0} -keyint_min {0}", - (int)(state.SegmentLength * state.TargetFramerate) + ((int)(state.SegmentLength * state.TargetFramerate)).ToString(CultureInfo.InvariantCulture) ); } @@ -982,6 +982,15 @@ namespace MediaBrowser.Api.Playback.Hls var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec); + if (state.BaseRequest.BreakOnNonKeyFrames) + { + // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe + // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable + // to produce a missing part of video stream before first keyframe is encountered, which may lead to + // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js + Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request"); + state.BaseRequest.BreakOnNonKeyFrames = false; + } var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions); // If isEncoding is true we're actually starting ffmpeg From adccc18298f4ebba511b68d5af9e72f78de5e5e8 Mon Sep 17 00:00:00 2001 From: Vasily Date: Mon, 14 Oct 2019 13:15:41 +0300 Subject: [PATCH 14/60] Revert "yarn build" as it is fixed in master, fix typo --- MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs | 2 +- deployment/debian-package-x64/docker-build.sh | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 1b93eaaa57..a95db53e42 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -931,7 +931,7 @@ namespace MediaBrowser.Api.Playback.Hls { // This is to make sure keyframe interval is limited to our segment, // as forcing keyframes is not enough. - // Example: we ecoded half of desired length, then codec detected + // Example: we encoded half of desired length, then codec detected // scene cut and inserted a keyframe; next forced keyframe would // be created outside of segment, which breaks seeking. keyFrameArg += string.Format( diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh index 5911923d78..97bc45a060 100755 --- a/deployment/debian-package-x64/docker-build.sh +++ b/deployment/debian-package-x64/docker-build.sh @@ -20,7 +20,6 @@ if [[ -n ${web_branch} ]]; then checkout -b origin/${web_branch} fi yarn install -yarn build mkdir -p ${web_target} mv dist/* ${web_target}/ popd From 9ad781324ebdfb9679a0dcf4258195a870c0826c Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Tue, 15 Oct 2019 04:24:30 -0400 Subject: [PATCH 15/60] Fix up Setup Type dialog, and add logic for basic setup Fix up the Setup Type dialog, by removing an unused function from the page. Adds logic to the installer to check for a basic or advanced setup choice. Ensures that the service is only installed if the "advanced" method is chosen. Note - it may be possible to remove some defaults from the .onInit, considering they get set through the program now. --- deployment/windows/dialogs/setuptype.nsdinc | 6 +--- deployment/windows/jellyfin.nsi | 34 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/deployment/windows/dialogs/setuptype.nsdinc b/deployment/windows/dialogs/setuptype.nsdinc index 0b171da282..6c30119331 100644 --- a/deployment/windows/dialogs/setuptype.nsdinc +++ b/deployment/windows/dialogs/setuptype.nsdinc @@ -49,8 +49,4 @@ Function fnc_setuptype_Create FunctionEnd -; dialog show function -Function fnc_setuptype_Show - Call fnc_setuptype_Create - nsDialogs::Show -FunctionEnd + diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi index b8de4a4401..375c3cd798 100644 --- a/deployment/windows/jellyfin.nsi +++ b/deployment/windows/jellyfin.nsi @@ -89,9 +89,10 @@ ShowUninstDetails show !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL ; Setup Type Page - Page custom ShowSetupTypePage ;SetupTypePage_Config + Page custom ShowSetupTypePage SetupTypePage_Config ; Components Page + !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage !insertmacro MUI_PAGE_COMPONENTS !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog @@ -185,8 +186,8 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer WriteUninstaller "$INSTDIR\Uninstall.exe" SectionEnd -Section /o "Jellyfin Server Service" InstallService - +Section "Jellyfin Server Service" InstallService +${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service! ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 DetailPrint "Jellyfin Server service statuscode, $0" ${If} $0 == 0 @@ -246,6 +247,7 @@ Section /o "Jellyfin Server Service" InstallService ${EndIf} DetailPrint "Jellyfin Server service account change, $0" ${EndIf} +${EndIf} SectionEnd @@ -327,7 +329,7 @@ SectionEnd Function .onInit ; Setting up defaults - StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_INSTALLSERVICE_ "Yes" StrCpy $_SERVICESTART_ "Yes" StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" StrCpy $_EXISTINGINSTALLATION_ "No" @@ -420,7 +422,13 @@ Function HideConfirmationPage FunctionEnd Function HideSetupTypePage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType + Abort + ${EndIf} +FunctionEnd + +Function HideComponentsPage + ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice Abort ${EndIf} FunctionEnd @@ -450,6 +458,22 @@ FunctionEnd Var StartServiceAfterInstall Var UseNetworkServiceAccount Var UseLocalSystemAccount +Var BasicInstall + + +Function SetupTypePage_Config +${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall +${If} $BasicInstall == 1 + StrCpy $_SETUPTYPE_ "Basic" + StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_SERVICESTART_ "No" + StrCpy $_SERVICEACCOUNTTYPE_ "None" +${Else} + StrCpy $_SETUPTYPE_ "Advanced" + StrCpy $_INSTALLSERVICE_ "Yes" +${EndIf} + +FunctionEnd Function ServiceConfigPage_Config ${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall From f80343bf9dfa6d79df85b12cdd66d3602c269ebc Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Wed, 16 Oct 2019 01:43:53 -0400 Subject: [PATCH 16/60] Fix the Setup Type dialog titles I had put the title and subtitle in the wrong order. This corrects the issue. --- deployment/windows/dialogs/setuptype.nsddef | 2 +- deployment/windows/dialogs/setuptype.nsdinc | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/deployment/windows/dialogs/setuptype.nsddef b/deployment/windows/dialogs/setuptype.nsddef index ff59f62152..b55ceeaaa6 100644 --- a/deployment/windows/dialogs/setuptype.nsddef +++ b/deployment/windows/dialogs/setuptype.nsddef @@ -4,7 +4,7 @@ This file was created by NSISDialogDesigner 1.4.4.0 http://coolsoft.altervista.org/nsisdialogdesigner Do not edit manually! --> - +