public override async Task <(ResultType result, List <DownloadInfo> downloadInfos)> GetFileUrlsOfVod(CancellationToken cancellationToken) { (bool retrieveVideoSuccess, HitboxVideo video) = await Hitbox.RetrieveVideo(VideoInfo.VideoId); if (!retrieveVideoSuccess) { return(ResultType.Failure, null); } VideoInfo = new HitboxVideoInfo(video); // TODO: Figure out how to determine quality when there are multiple. string m3u8path = "https://smashcast-vod.akamaized.net/static/videos/vods" + GetM3U8PathFromM3U(video.MediaProfiles.First().Url); string folderpath = TsVideoJob.GetFolder(m3u8path); string m3u8; HttpClient client = new HttpClient(); HttpResponseMessage response = await client.GetAsync(new Uri( m3u8path )); if (response.StatusCode == System.Net.HttpStatusCode.OK) { m3u8 = await response.Content.ReadAsStringAsync(); } else { return(ResultType.Failure, null); } // who cares, hitbox is dead return(ResultType.Failure, null); //string[] filenames = TsVideoJob.GetFilenamesFromM3U8( m3u8 ); //List<string> urls = new List<string>( filenames.Length ); //foreach ( var filename in filenames ) { // urls.Add( folderpath + filename ); //} //return (ResultType.Success, urls.ToArray()); }
public override async Task <(ResultType result, List <DownloadInfo> downloadInfos)> GetFileUrlsOfVod(CancellationToken cancellationToken) { var video_json = await TwitchYTDL.GetVideoJson(long.Parse(VideoInfo.VideoId)); VideoInfo = new TwitchVideoInfo(TwitchYTDL.VideoFromJson(video_json)); string folderpath; List <DownloadInfo> downloadInfos; while (true) { try { bool interactive = false; if (interactive) { Status = ""; string tmp1 = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_baseurl.txt"); string tmp2 = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_tsnames.txt"); string linesbaseurl = TryGetUserCopyBaseurlM3U(tmp1); string linestsnames = TryGetUserCopyTsnamesM3U(tmp2); if (linesbaseurl == null) { File.WriteAllText(tmp1, "get baseurl for m3u8 from https://www.twitch.tv/videos/" + VideoInfo.VideoId); } if (linestsnames == null) { File.WriteAllText(tmp2, "get actual m3u file from https://www.twitch.tv/videos/" + VideoInfo.VideoId); } if (linesbaseurl == null || linestsnames == null) { await Task.Delay(200); Process.Start(tmp1); await Task.Delay(200); Process.Start(tmp2); await Task.Delay(200); return(ResultType.UserInputRequired, null); } folderpath = TsVideoJob.GetFolder(GetM3U8PathFromM3U(linesbaseurl, VideoQuality)); downloadInfos = TsVideoJob.GetFilenamesFromM3U8(folderpath, linestsnames); } else { string m3u8path = ExtractM3u8FromJson(video_json); folderpath = TsVideoJob.GetFolder(m3u8path); var client = new System.Net.Http.HttpClient(); var result = await client.GetAsync(m3u8path); string m3u8 = await result.Content.ReadAsStringAsync(); downloadInfos = TsVideoJob.GetFilenamesFromM3U8(folderpath, m3u8); } } catch (TwitchHttpException e) { if (e.StatusCode == System.Net.HttpStatusCode.NotFound && VideoInfo.VideoRecordingState == RecordingState.Live) { // this can happen on streams that have just started, in this just wait a bit and retry try { await Task.Delay(20000, cancellationToken); } catch (TaskCanceledException) { return(ResultType.Cancelled, null); } video_json = await TwitchYTDL.GetVideoJson(long.Parse(VideoInfo.VideoId)); VideoInfo = new TwitchVideoInfo(TwitchYTDL.VideoFromJson(video_json)); continue; } else { throw; } } break; } return(ResultType.Success, downloadInfos); }
public UserInputRequestStreamLive(TsVideoJob job) { Job = job; }
public UserInputRequestTimeMismatchRemuxed(TsVideoJob job) { Job = job; }
public UserInputRequestTimeMismatchCombined(TsVideoJob job) { Job = job; }
public override async Task <ResultType> Run(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } JobStatus = VideoJobStatus.Running; Status = "Retrieving video info..."; (ResultType getFileUrlsResult, List <DownloadInfo> downloadInfos) = await GetFileUrlsOfVod(cancellationToken); if (getFileUrlsResult == ResultType.UserInputRequired) { Status = "Need manual fetch of file URLs."; return(ResultType.UserInputRequired); } if (getFileUrlsResult != ResultType.Success) { Status = "Failed retrieving file URLs."; return(ResultType.Failure); } string combinedTempname = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_combined.ts"); string combinedFilename = Path.Combine(GetTempFolder(), GetFinalFilenameWithoutExtension() + ".ts"); string remuxedTempname = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_combined.mp4"); string remuxedFilename = Path.Combine(GetTempFolder(), GetFinalFilenameWithoutExtension() + ".mp4"); string targetFilename = Path.Combine(GetTargetFolder(), GetFinalFilenameWithoutExtension() + ".mp4"); string baseurlfilepath = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_baseurl.txt"); string tsnamesfilepath = Path.Combine(GetTempFolder(), GetTempFilenameWithoutExtension() + "_tsnames.txt"); if (!await Util.FileExists(targetFilename)) { if (!await Util.FileExists(combinedFilename)) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } Status = "Downloading files..."; string[] files; while (true) { if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); timer.Start(); var downloadResult = await Download(this, cancellationToken, GetTempFolderForParts(), downloadInfos); if (downloadResult.result != ResultType.Success) { return(downloadResult.result); } files = downloadResult.files; if (this.AssumeFinished || this.VideoInfo.VideoRecordingState != RecordingState.Live) { break; } else { // we're downloading a stream that is still streaming timer.Stop(); // if too little time has passed wait a bit to allow the stream to provide new data if (timer.Elapsed.TotalMinutes < 2.5) { TimeSpan ts = TimeSpan.FromMinutes(2.5) - timer.Elapsed; Status = "Waiting " + ts.TotalSeconds + " seconds for stream to update..."; _UserInputRequest = new UserInputRequestStreamLive(this); try { await Task.Delay(ts, cancellationToken); } catch (TaskCanceledException) { return(ResultType.Cancelled); } } if (File.Exists(tsnamesfilepath)) { File.Delete(tsnamesfilepath); } (getFileUrlsResult, downloadInfos) = await GetFileUrlsOfVod(cancellationToken); if (getFileUrlsResult != ResultType.Success) { Status = "Failed retrieving file URLs."; return(ResultType.Failure); } } } _UserInputRequest = null; Status = "Waiting for free disk IO slot to combine..."; try { await Util.ExpensiveDiskIOSemaphore.WaitAsync(cancellationToken); } catch (OperationCanceledException) { return(ResultType.Cancelled); } try { long expectedTargetFilesize = 0; foreach (var file in files) { expectedTargetFilesize += new FileInfo(file).Length; } Status = "Combining downloaded video parts..."; if (await Util.FileExists(combinedTempname)) { await Util.DeleteFile(combinedTempname); } await StallWrite(combinedFilename, expectedTargetFilesize, cancellationToken); if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } ResultType combineResult = await TsVideoJob.Combine(cancellationToken, combinedTempname, files); if (combineResult != ResultType.Success) { return(combineResult); } // sanity check Status = "Sanity check on combined video..."; TimeSpan actualVideoLength = (await FFMpegUtil.Probe(combinedTempname)).Duration; TimeSpan expectedVideoLength = VideoInfo.VideoLength; if (!IgnoreTimeDifferenceCombined && actualVideoLength.Subtract(expectedVideoLength).Duration() > TimeSpan.FromSeconds(5)) { // if difference is bigger than 5 seconds something is off, report Status = "Large time difference between expected (" + expectedVideoLength.ToString() + ") and combined (" + actualVideoLength.ToString() + "), stopping."; _UserInputRequest = new UserInputRequestTimeMismatchCombined(this); _IsWaitingForUserInput = true; return(ResultType.UserInputRequired); } Util.MoveFileOverwrite(combinedTempname, combinedFilename); await Util.DeleteFiles(files); System.IO.Directory.Delete(GetTempFolderForParts()); } finally { Util.ExpensiveDiskIOSemaphore.Release(); } } Status = "Waiting for free disk IO slot to remux..."; try { await Util.ExpensiveDiskIOSemaphore.WaitAsync(cancellationToken); } catch (OperationCanceledException) { return(ResultType.Cancelled); } try { Status = "Remuxing to MP4..."; if (await Util.FileExists(remuxedTempname)) { await Util.DeleteFile(remuxedTempname); } await StallWrite(remuxedFilename, new FileInfo( combinedFilename ).Length, cancellationToken); if (cancellationToken.IsCancellationRequested) { return(ResultType.Cancelled); } await Task.Run(() => TsVideoJob.Remux(remuxedFilename, combinedFilename, remuxedTempname)); // sanity check Status = "Sanity check on remuxed video..."; TimeSpan actualVideoLength = (await FFMpegUtil.Probe(remuxedFilename)).Duration; TimeSpan expectedVideoLength = VideoInfo.VideoLength; if (!IgnoreTimeDifferenceRemuxed && actualVideoLength.Subtract(expectedVideoLength).Duration() > TimeSpan.FromSeconds(5)) { // if difference is bigger than 5 seconds something is off, report Status = "Large time difference between expected (" + expectedVideoLength.ToString() + ") and remuxed (" + actualVideoLength.ToString() + "), stopping."; _UserInputRequest = new UserInputRequestTimeMismatchRemuxed(this); _IsWaitingForUserInput = true; return(ResultType.UserInputRequired); } Util.MoveFileOverwrite(remuxedFilename, targetFilename); } finally { Util.ExpensiveDiskIOSemaphore.Release(); } } Status = "Done!"; JobStatus = VideoJobStatus.Finished; if (File.Exists(tsnamesfilepath)) { File.Delete(tsnamesfilepath); } if (File.Exists(baseurlfilepath)) { File.Delete(baseurlfilepath); } return(ResultType.Success); }