private void DownloadOutputDataReceived(DataReceivedEventArgs eventArgs, VideoConfig video) { _downloadLog += eventArgs.Data + "\r\n"; Log.Debug(eventArgs.Data); ParseDownloadProgress(video, eventArgs); DownloadProgress?.Invoke(video); }
private static void ParseDownloadProgress(VideoConfig video, DataReceivedEventArgs dataReceivedEventArgs) { if (dataReceivedEventArgs.Data == null) { return; } var rx = new Regex(@"(\d*).\d%+"); var match = rx.Match(dataReceivedEventArgs.Data); if (!match.Success) { if (dataReceivedEventArgs.Data.Contains("Converting video")) { video.DownloadState = DownloadState.Converting; } return; } var ci = (CultureInfo)CultureInfo.CurrentCulture.Clone(); ci.NumberFormat.NumberDecimalSeparator = "."; video.DownloadProgress = float.Parse(match.Value.Substring(0, match.Value.Length - 1), ci) / 100; }
private static void PrepareClonedScreens(VideoConfig videoConfig) { var screenCount = PlaybackController.Instance.gameObject.transform.childCount; if (screenCount <= 1) { return; } Log.Debug($"Screens found: {screenCount}"); foreach (Transform screen in PlaybackController.Instance.gameObject.transform) { if (!screen.name.StartsWith("CinemaScreen")) { return; } if (screen.name.Contains("Clone")) { PlaybackController.Instance.VideoPlayer.screenController.Screens.Add(screen.gameObject); screen.GetComponent <Renderer>().material = PlaybackController.Instance.VideoPlayer.screenController.Screens[0].GetComponent <Renderer>().material; Object.Destroy(screen.Find("CinemaDirectionalLight").gameObject); } screen.gameObject.GetComponent <CustomBloomPrePass>().enabled = false; Log.Debug("Disabled bloom prepass"); } PlaybackController.Instance.VideoPlayer.SetPlacement( Placement.CreatePlacementForConfig(videoConfig, PlaybackController.Scene.SoloGameplay, PlaybackController.Instance.VideoPlayer.GetVideoAspectRatio()) ); PlaybackController.Instance.VideoPlayer.screenController.SetShaderParameters(videoConfig); }
private void DownloadProcessExited(Process process, VideoConfig video) { var exitCode = process.ExitCode; if (exitCode != 0) { Log.Warn(_downloadLog.Length > 0 ? _downloadLog : "Empty youtube-dl log"); video.DownloadState = DownloadState.Cancelled; } Log.Info($"Download process exited with code {exitCode}"); if (video.DownloadState == DownloadState.Cancelled) { Log.Info("Cancelled download"); VideoLoader.DeleteVideo(video); DownloadFinished?.Invoke(video); } else { process.Disposed -= DownloadProcessDisposed; _downloadProcesses.TryRemove(video, out _); video.DownloadState = DownloadState.Downloaded; video.NeedsToSave = true; SharedCoroutineStarter.instance.StartCoroutine(WaitForDownloadToFinishCoroutine(video)); Log.Info("Download finished"); } }
public static bool DeleteConfig(VideoConfig videoConfig, IPreviewBeatmapLevel level) { if (videoConfig.LevelDir == null) { Log.Error("LevelDir was null when trying to delete config"); return(false); } try { var cinemaConfigPath = Path.Combine(videoConfig.LevelDir, CONFIG_FILENAME); if (File.Exists(cinemaConfigPath)) { File.Delete(cinemaConfigPath); } var mvpConfigPath = Path.Combine(videoConfig.LevelDir, CONFIG_FILENAME_MVP); if (File.Exists(mvpConfigPath)) { File.Delete(mvpConfigPath); } } catch (Exception e) { Log.Error("Failed to delete video config:"); Log.Error(e); } RemoveConfigFromCache(level); Log.Info("Deleted video config"); return(true); }
public static void DeleteVideo(VideoConfig videoConfig) { if (videoConfig.VideoPath == null) { Log.Warn("Tried to delete video, but its path was null"); return; } try { File.Delete(videoConfig.VideoPath); Log.Info("Deleted video at " + videoConfig.VideoPath); if (videoConfig.DownloadState != DownloadState.Cancelled) { videoConfig.DownloadState = DownloadState.NotDownloaded; } videoConfig.videoFile = null; } catch (Exception e) { Log.Error("Failed to delete video at " + videoConfig.VideoPath); Log.Error(e); } }
public static void SaveVideoConfig(VideoConfig videoConfig) { if (videoConfig.LevelDir == null || !Directory.Exists(videoConfig.LevelDir)) { Log.Warn("Failed to save video. Path " + videoConfig.LevelDir + " does not exist."); return; } if (videoConfig.IsWIPLevel) { videoConfig.configByMapper = true; } var videoJsonPath = Path.Combine(videoConfig.LevelDir, CONFIG_FILENAME); _ignoreNextEventForPath = videoJsonPath; Log.Info($"Saving video config to {videoJsonPath}"); try { File.WriteAllText(videoJsonPath, JsonConvert.SerializeObject(videoConfig, Formatting.Indented)); videoConfig.NeedsToSave = false; } catch (Exception e) { Log.Error("Failed to save level data: "); Log.Error(e); } }
private IEnumerator WaitForDownloadToFinishCoroutine(VideoConfig video) { var timeout = new Timeout(3); yield return(new WaitUntil(() => timeout.HasTimedOut || File.Exists(video.VideoPath))); DownloadFinished?.Invoke(video); }
public static void AddConfigToCache(VideoConfig config, IPreviewBeatmapLevel level) { var success = CachedConfigs.TryAdd(level.levelID, config); if (success) { Log.Debug($"Adding config for {level.levelID} to cache"); } }
private IEnumerator DownloadVideoCoroutine(VideoConfig video, VideoQuality.Mode quality) { Log.Info($"Starting download of {video.title}"); _downloadLog = ""; var downloadProcess = CreateDownloadProcess(video, quality); if (downloadProcess == null) { yield break; } video.DownloadState = DownloadState.Downloading; DownloadProgress?.Invoke(video); Log.Info( $"youtube-dl command: \"{downloadProcess.StartInfo.FileName}\" {downloadProcess.StartInfo.Arguments}"); var timeout = new Timeout(5 * 60); downloadProcess.OutputDataReceived += (sender, e) => UnityMainThreadTaskScheduler.Factory.StartNew(delegate { DownloadOutputDataReceived(e, video); }); downloadProcess.ErrorDataReceived += (sender, e) => UnityMainThreadTaskScheduler.Factory.StartNew(delegate { DownloadErrorDataReceived(e); }); downloadProcess.Exited += (sender, e) => UnityMainThreadTaskScheduler.Factory.StartNew(delegate { DownloadProcessExited((Process)sender, video); }); downloadProcess.Disposed += (sender, e) => UnityMainThreadTaskScheduler.Factory.StartNew(delegate { DownloadProcessDisposed((Process)sender, e); }); StartProcessThreaded(downloadProcess); var startProcessTimeout = new Timeout(10); yield return(new WaitUntil(() => IsProcessRunning(downloadProcess) || startProcessTimeout.HasTimedOut)); startProcessTimeout.Stop(); yield return(new WaitUntil(() => !IsProcessRunning(downloadProcess) || timeout.HasTimedOut)); if (timeout.HasTimedOut) { Log.Warn("Timeout reached, disposing download process"); } else { //When the download is finished, wait for process to exit instead of immediately killing it yield return(new WaitForSeconds(2f)); } timeout.Stop(); _downloadLog = ""; DisposeProcess(downloadProcess); }
public void PrepareVideo(VideoConfig video) { _previewWaitingForVideoPlayer = true; if (_prepareVideoCoroutine != null) { StopCoroutine(_prepareVideoCoroutine); } _prepareVideoCoroutine = PrepareVideoCoroutine(video); StartCoroutine(_prepareVideoCoroutine); }
public void CancelDownload(VideoConfig video) { Log.Debug("Cancelling download"); video.DownloadState = DownloadState.Cancelled; DownloadProgress?.Invoke(video); var success = _downloadProcesses.TryGetValue(video, out var process); if (success) { DisposeProcess(process); } VideoLoader.DeleteVideo(video); }
private static void CreateAdditionalScreens(VideoConfig videoConfig) { if (videoConfig.additionalScreens == null) { return; } var screenController = PlaybackController.Instance.VideoPlayer.screenController; var i = 0; foreach (var _ in videoConfig.additionalScreens) { var clone = Object.Instantiate(screenController.Screens[0], screenController.Screens[0].transform.parent); clone.name += $" ({i++.ToString()})"; } }
private IEnumerator PrepareVideoCoroutine(VideoConfig video) { VideoConfig = video; VideoPlayer.Pause(); if (!(VideoConfig.DownloadState == DownloadState.Downloaded || VideoConfig.IsStreamable)) { Log.Debug("Video is not downloaded, stopping prepare"); VideoPlayer.FadeOut(); yield break; } VideoPlayer.LoopVideo(video.loop == true); VideoPlayer.screenController.SetShaderParameters(video); VideoPlayer.SetBloomIntensity(video.bloom); if (video.VideoPath == null) { Log.Debug("Video path was null, stopping prepare"); yield break; } var videoPath = video.VideoPath; Log.Info($"Loading video: {videoPath}"); if (VideoConfig.IsLocal) { var videoFileInfo = new FileInfo(videoPath); var timeout = new Timeout(0.25f); if (VideoPlayer.Url != videoPath) { yield return(new WaitUntil(() => !Util.IsFileLocked(videoFileInfo) || timeout.HasTimedOut)); } timeout.Stop(); if (timeout.HasTimedOut && Util.IsFileLocked(videoFileInfo)) { Log.Warn("Video file locked"); } } VideoPlayer.Url = videoPath; VideoPlayer.Prepare(); }
private static VideoConfig?LoadConfig(string configPath) { if (!File.Exists(configPath)) { Log.Warn($"Config file {configPath} does not exist"); return(null); } VideoConfig?videoConfig; try { var json = File.ReadAllText(configPath); if (configPath.EndsWith("\\" + CONFIG_FILENAME_MVP)) { //Back compatiblity with MVP configs var videoConfigListBackCompat = JsonConvert.DeserializeObject <VideoConfigListBackCompat>(json); videoConfig = new VideoConfig(videoConfigListBackCompat); } else { videoConfig = JsonConvert.DeserializeObject <VideoConfig>(json); } } catch (Exception e) { Log.Error($"Error parsing video json {configPath}:"); Log.Error(e); return(null); } // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (videoConfig != null) { videoConfig.LevelDir = Path.GetDirectoryName(configPath); videoConfig.UpdateDownloadState(); } else { Log.Warn($"Deserializing video config at {configPath} failed"); } return(videoConfig); }
public static string ToYoutubeDLFormat(VideoConfig config, Mode quality) { string?qualityString; if (config.videoUrl == null || config.videoUrl.StartsWith("https://www.youtube.com/watch")) { qualityString = $"bestvideo[height<={(int) quality}][vcodec*=avc1]+bestaudio[acodec*=mp4]"; } else if (config.videoUrl.StartsWith("https://vimeo.com/")) { qualityString = $"bestvideo[height<={(int) quality}][vcodec*=avc1]+bestaudio[acodec*=mp4]"; } else if (config.videoUrl.StartsWith("https://www.facebook.com")) { qualityString = "mp4"; } else { qualityString = $"best[height<={(int) quality}][vcodec*=avc1]"; } return(qualityString); }
private void PlayVideo(float startTime) { if (VideoConfig == null) { Log.Warn("VideoConfig null in PlayVideo"); return; } VideoPlayer.IsSyncing = false; // Always hide screen body in the menu, since the drawbacks of the body being visible are large if (VideoConfig.TransparencyEnabled && _activeScene != Scene.Menu) { VideoPlayer.ShowScreenBody(); } else { VideoPlayer.HideScreenBody(); } var totalOffset = VideoConfig.GetOffsetInSec(); var songSpeed = 1f; if (BS_Utils.Plugin.LevelData.IsSet) { songSpeed = BS_Utils.Plugin.LevelData.GameplayCoreSceneSetupData.gameplayModifiers.songSpeedMul; if (BS_Utils.Plugin.LevelData.GameplayCoreSceneSetupData?.practiceSettings != null) { songSpeed = BS_Utils.Plugin.LevelData.GameplayCoreSceneSetupData.practiceSettings.songSpeedMul; if ((totalOffset + startTime) < 0) { totalOffset /= (songSpeed * VideoConfig.PlaybackSpeed); } } } VideoPlayer.PlaybackSpeed = songSpeed * VideoConfig.PlaybackSpeed; totalOffset += startTime; //This must happen after song speed adjustment if ((songSpeed * VideoConfig.PlaybackSpeed) < 1f && totalOffset > 0f) { //Unity crashes if the playback speed is less than 1 and the video time at the start of playback is greater than 0 Log.Warn("Video playback disabled to prevent Unity crash"); VideoPlayer.Hide(); StopPlayback(); return; } //Video seemingly always lags behind. A fixed offset seems to work well enough if (!IsPreviewPlaying) { totalOffset += 0.0667f; } if (VideoConfig.endVideoAt != null && totalOffset > VideoConfig.endVideoAt) { totalOffset = VideoConfig.endVideoAt.Value; } //This will fail if the video is not prepared yet if (VideoPlayer.VideoDuration > 0) { totalOffset %= VideoPlayer.VideoDuration; } //This fixes an issue where the Unity video player sometimes ignores a change in the .time property if the time is very small and the player is currently playing if (Math.Abs(totalOffset) < 0.001f) { totalOffset = 0; Log.Debug("Set very small offset to 0"); } Log.Debug($"Total offset: {totalOffset}, startTime: {startTime}, songSpeed: {songSpeed}, player time: {VideoPlayer.Player.time}"); StopAllCoroutines(); if (_activeAudioSource != null) { _lastKnownAudioSourceTime = _activeAudioSource.time; } if (totalOffset < 0) { if (!IsPreviewPlaying) { //Negate the offset to turn it into a positive delay StartCoroutine(PlayVideoDelayedCoroutine(-(totalOffset))); } else { //In menus we don't need to wait, instead the preview player starts earlier VideoPlayer.Play(); } } else { VideoPlayer.Play(); if (!VideoPlayer.Player.isPrepared) { _audioSourceStartTime = DateTime.Now; _offsetAfterPrepare = totalOffset; } else { VideoPlayer.Player.time = totalOffset; } } }
public async void StartPreview() { if (VideoConfig == null || _currentLevel == null) { Log.Warn("No video or level selected in OnPreviewAction"); return; } if (IsPreviewPlaying) { Log.Debug("Stopping preview"); StopPreview(true); } else { Log.Debug("Starting preview"); IsPreviewPlaying = true; if (VideoPlayer.IsPlaying) { StopPlayback(); } if (!VideoPlayer.IsPrepared) { Log.Debug("Video not prepared yet"); } //Start the preview at the point the video kicks in var startTime = 0f; if (VideoConfig.offset < 0) { startTime = -VideoConfig.GetOffsetInSec(); } if (SongPreviewPlayerController.SongPreviewPlayer == null) { Log.Error("Failed to get reference to SongPreviewPlayer during preview"); return; } try { Log.Debug($"Preview start time: {startTime}, offset: {VideoConfig.GetOffsetInSec()}"); var audioClip = await VideoLoader.GetAudioClipForLevel(_currentLevel); if (audioClip != null) { SongPreviewPlayerController.SongPreviewPlayer.CrossfadeTo(audioClip, -5f, startTime, _currentLevel.songDuration, null); } else { Log.Error("AudioClip for level failed to load"); } } catch (Exception e) { Log.Error(e); IsPreviewPlaying = false; return; } //+1.0 is hard right. only pan "mostly" right, because for some reason the video player audio doesn't //pan hard left either. Also, it sounds a bit more comfortable. SetAudioSourcePanning(0.9f); StartCoroutine(PlayVideoAfterAudioSourceCoroutine(true)); VideoPlayer.PanStereo = -1f; // -1 is hard left VideoPlayer.Unmute(); } }
private Process?CreateDownloadProcess(VideoConfig video, VideoQuality.Mode quality) { if (video.LevelDir == null) { Log.Error("LevelDir was null during download"); return(null); } var success = _downloadProcesses.TryGetValue(video, out _); if (success) { Log.Warn("Existing process not cleaned up yet. Cancelling download attempt."); return(null); } if (!Directory.Exists(video.LevelDir)) { //Needed for OST videos Directory.CreateDirectory(video.LevelDir); } var videoFileName = Util.ReplaceIllegalFilesystemChars(video.title ?? video.videoID ?? "video"); videoFileName = Util.ShortenFilename(video.LevelDir, videoFileName); video.videoFile = videoFileName + ".mp4"; string videoUrl; if (video.videoUrl != null) { if (UrlInWhitelist(video.videoUrl)) { videoUrl = video.videoUrl; } else { Log.Error($"Video hoster for {video.videoUrl} is not allowed"); return(null); } } else if (video.videoID != null) { videoUrl = $"https://www.youtube.com/watch?v={video.videoID}"; } else { Log.Error("Video config has neither videoID or videoUrl set"); return(null); } var videoFormat = VideoQuality.ToYoutubeDLFormat(video, quality); videoFormat = $" -f \"{videoFormat}\""; var downloadProcessArguments = videoUrl + videoFormat + " --no-cache-dir" + // Don't use temp storage $" -o \"{videoFileName}.%(ext)s\"" + " --no-playlist" + // Don't download playlists, only the first video " --no-part" + // Don't store download in parts, write directly to file " --recode-video mp4" + //Re-encode to mp4 (will be skipped most of the time, since it's already in an mp4 container) " --no-mtime" + //Video last modified will be when it was downloaded, not when it was uploaded to youtube " --socket-timeout 10"; //Retry if no response in 10 seconds Note: Not if download takes more than 10 seconds but if the time between any 2 messages from the server is 10 seconds var process = CreateProcess(downloadProcessArguments, video.LevelDir); _downloadProcesses.TryAdd(video, process); return(process); }
public void StartDownload(VideoConfig video, VideoQuality.Mode quality) { SharedCoroutineStarter.instance.StartCoroutine(DownloadVideoCoroutine(video, quality)); }