Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 230 additions & 11 deletions Packages/com.texelsaur.video/Editor/EditorUrlResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,64 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

namespace Texel.Video.Internal
{
public class VideoMeta
{
public string id;
public Double duration;
}

/// <summary>
/// Allows people to put in links to YouTube videos and other supported video services and have links just work
/// Hooks into VRC's video player URL resolve callback and uses the VRC installation of YouTubeDL to resolve URLs in the editor.
/// </summary>
public static class EditorUrlResolver
{
private static string _youtubeDLPath = "";
private static string _ffmpegPath = "";
private static string _ytdlResolvedURL = "";
private static VideoMeta _ytdlJson;
private static string _ffmpegError;
private const string _ffmpegCache = "Video Cache";
private const string _ffErrorIdentifier = ", from 'http";

#if UNITY_EDITOR_WIN
private const int _ytdlArgsCount = 7;
#else
private const int _ytdlArgsCount = 8;
#endif
private static System.Diagnostics.Process _ffmpegProcess;
private static HashSet<System.Diagnostics.Process> _runningYtdlProcesses = new HashSet<System.Diagnostics.Process>();
private static HashSet<MonoBehaviour> _registeredBehaviours = new HashSet<MonoBehaviour>();

private static System.Diagnostics.Process ResolvingProcess(string resolverPath, string[] args)
{
System.Diagnostics.Process resolver = new System.Diagnostics.Process();

resolver.EnableRaisingEvents = true;

resolver.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
resolver.StartInfo.CreateNoWindow = true;
resolver.StartInfo.UseShellExecute = false;
resolver.StartInfo.RedirectStandardInput = true;
resolver.StartInfo.RedirectStandardOutput = true;
resolver.StartInfo.RedirectStandardError = true;

resolver.StartInfo.FileName = resolverPath;

foreach (string argument in args)
resolver.StartInfo.Arguments += argument + " ";

return resolver;
}

private static string SanitizeURL(string url, string identifier, char seperator)
{
if (url.StartsWith(identifier) && url.Contains(seperator))
url = url.Substring(0, url.IndexOf(seperator));

return url;
}

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void SetupURLResolveCallback()
{
Expand All @@ -61,6 +109,15 @@ private static void SetupURLResolveCallback()
_youtubeDLPath = string.Join("\\", splitPath.Take(splitPath.Length - 2)) + "\\VRChat\\VRChat\\Tools\\youtube-dl.exe";
}

#if UNITY_EDITOR_LINUX
if (!File.Exists(_youtubeDLPath))
_youtubeDLPath = "/usr/bin/yt-dlp";

_ffmpegPath = "/usr/bin/ffmpeg";
if (!File.Exists(_ffmpegPath))
Debug.LogWarning("[<color=#A7D147>VideoTXL FFMPEG</color>] Unable to find FFmpeg installation, URLs will not be transcoded in editor test your videos in game.");
#endif

if (!File.Exists(_youtubeDLPath))
{
Debug.LogWarning("[<color=#A7D147>VideoTXL YTDL</color>] Unable to find VRC YouTube-DL or YT-DLP installation, URLs will not be resolved in editor test your videos in game.");
Expand Down Expand Up @@ -109,41 +166,203 @@ private static void ResolveURLCallback(VRCUrl url, int resolution, UnityEngine.O
// return;
//}

var ytdlProcess = new System.Diagnostics.Process();
// Catch playlist runaway
string urls = SanitizeURL(url.ToString(), "https://www.youtube.com/", '&');
urls = SanitizeURL(urls, "https://youtu.be/", '?');

ytdlProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
ytdlProcess.StartInfo.CreateNoWindow = true;
ytdlProcess.StartInfo.UseShellExecute = false;
ytdlProcess.StartInfo.RedirectStandardOutput = true;
ytdlProcess.StartInfo.FileName = _youtubeDLPath;
ytdlProcess.StartInfo.Arguments = $"--no-check-certificate --no-cache-dir --rm-cache-dir -f \"mp4[height<=?{resolution}][protocol=https]/best[height<=?{resolution}][protocol=https]\" --get-url \"{url}\"";
if (_ffmpegProcess != null)
{
_ffmpegProcess.StandardInput.Write('q');
_ffmpegProcess.StandardInput.Flush();
}

string[] ytdlpArgs = new string[_ytdlArgsCount] {
"--no-check-certificate",
"--no-cache-dir",
"--rm-cache-dir",
#if !UNITY_EDITOR_WIN
"--dump-json",
#endif

"-f", $"\"mp4[height<=?{resolution}][protocol^=http]/best[height<=?{resolution}][protocol^=http]\"",

"--get-url", $"\"{urls}\""
};

System.Diagnostics.Process ytdlProcess = ResolvingProcess(_youtubeDLPath, ytdlpArgs);

Debug.Log($"[<color=#A7D147>VideoTXL YTDL</color>] Attempting to resolve URL '{url}'");
Debug.Log($"[<color=#A7D147>VideoTXL YTDL</color>] Attempting to resolve URL '{urls}'");

ytdlProcess.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
{
if (args.Data.StartsWith("{"))
{
#if UNITY_EDITOR_WIN
_ytdlJson = new VideoMeta();
_ytdlJson.id = urls;
_ytdlJson.duration = 0;
#else
_ytdlJson = JsonUtility.FromJson<VideoMeta>(args.Data);
#endif
}
else
{
_ytdlResolvedURL = args.Data;
}
}
};

ytdlProcess.Start();
ytdlProcess.BeginOutputReadLine();

_runningYtdlProcesses.Add(ytdlProcess);

((MonoBehaviour)videoPlayer).StartCoroutine(URLResolveCoroutine(url.ToString(), ytdlProcess, videoPlayer, urlResolvedCallback, errorCallback));
((MonoBehaviour)videoPlayer).StartCoroutine(URLResolveCoroutine(urls, ytdlProcess, videoPlayer, urlResolvedCallback, errorCallback));

_registeredBehaviours.Add((MonoBehaviour)videoPlayer);
}

private static IEnumerator URLTranscodeCoroutine(string resolvedURL, string outputURL, string originalUrl, System.Diagnostics.Process ffmpegProcess, UnityEngine.Object videoPlayer, Action<string> urlResolvedCallback, Action<VideoError> errorCallback)
{
while (!ffmpegProcess.HasExited)
yield return new WaitForSeconds(0.1f);

if (File.Exists(outputURL))
{
Debug.Log($"[<color=#A7D147>VideoTXL FFMPEG</color>] Successfully transcoded URL '{originalUrl}'");

#if UNITY_EDITOR_WIN
urlResolvedCallback($"file:\\\\{outputURL}");
#else
urlResolvedCallback($"file://{outputURL}");
#endif
}
else
{
Debug.LogWarning($"[<color=#A7D147>VideoTXL FFMPEG</color>] Unable to transcode URL, '{originalUrl}' will not be played in editor test your videos in game.\n{_ffmpegError}");

errorCallback(VideoError.InvalidURL);
}

_ffmpegProcess.Dispose();
_ffmpegProcess = null;
}

private static IEnumerator URLResolveCoroutine(string originalUrl, System.Diagnostics.Process ytdlProcess, UnityEngine.Object videoPlayer, Action<string> urlResolvedCallback, Action<VideoError> errorCallback)
{
while (!ytdlProcess.HasExited)
yield return new WaitForSeconds(0.1f);

_runningYtdlProcesses.Remove(ytdlProcess);

string resolvedURL = ytdlProcess.StandardOutput.ReadLine();
string resolvedURL = _ytdlResolvedURL;

// If a URL fails to resolve, YTDL will send error to stderror and nothing will be output to stdout
if (string.IsNullOrEmpty(resolvedURL))
errorCallback(VideoError.InvalidURL);
else
{
Debug.Log($"[<color=#A7D147>VideoTXL YTDL</color>] Successfully resolved URL '{originalUrl}' to '{resolvedURL}'");
string debugStdout = resolvedURL;
if (resolvedURL.Contains("ip="))
{
int filterStart = resolvedURL.IndexOf("ip=");
int filterEnd = resolvedURL.Substring(filterStart).IndexOf("&");

debugStdout = resolvedURL.Replace(resolvedURL.Substring(filterStart + 3, filterEnd - 3), "[REDACTED]");
}
Debug.Log($"[<color=#A7D147>VideoTXL YTDL</color>] Successfully resolved URL '{originalUrl}' to '{debugStdout}'");

#if !UNITY_EDITOR_LINUX
urlResolvedCallback(resolvedURL);
#else

if (File.Exists(_ffmpegPath))
{
string tempPath = Path.GetFullPath(Path.Combine("Temp", _ffmpegCache));

if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);

string urlHash = Hash128.Compute(originalUrl).ToString();
string fullUrlHash = Path.Combine(tempPath, urlHash + ".webm");

if (File.Exists(fullUrlHash))
{
Debug.Log($"[<color=#A7D147>VideoTXL FFMPEG</color>] Loaded cached video '{originalUrl}'");
urlResolvedCallback(fullUrlHash);
}
else
{
string[] ffmpegArgs = new string[13] {
"-hide_banner",

"-y",

"-hwaccel auto",

"-i", $"\"{resolvedURL}\"",

"-c:a", $"{ "libvorbis" }",

"-c:v", $"{ "vp8" }",

"vp8" == "vp8" ? "-cpu-used 6 -deadline realtime -qmin 0 -qmax 50 -crf 5 -minrate 1M -maxrate 1M -b:v 1M" : "",

"-f", $"{ "webm" }",

$"\"{fullUrlHash}\""
};

_ffmpegError = "";

_ffmpegProcess = ResolvingProcess(_ffmpegPath, ffmpegArgs);

_ffmpegProcess.ErrorDataReceived += (sender, args) =>
{
if (args.Data != null)
{
if (args.Data == "Press [q] to stop, [?] for help")
Debug.Log($"[<color=#A7D147>VideoTXL FFMPEG</color>] Starting transcode '{originalUrl}'");
else if (args.Data.StartsWith("frame="))
{
string progressTimeString = args.Data;
int progressTimeIndex = progressTimeString.IndexOf("time=") + 5;
int progressTimeLength = progressTimeString.IndexOf("bitrate=") - progressTimeIndex;

string progressTime = progressTimeString.Substring(progressTimeIndex, progressTimeLength);
TimeSpan ffmpegProgress = TimeSpan.Parse(progressTime);

string progressSeconds = ffmpegProgress.ToString();
progressSeconds = progressSeconds.Contains('.') ? progressSeconds.Substring(0, progressSeconds.IndexOf('.')) : progressSeconds;
progressSeconds += "s";
string progressPercent = _ytdlJson.duration == 0.0 ? "" : $"- {Mathf.FloorToInt((float)(ffmpegProgress.TotalSeconds / _ytdlJson.duration) * 100f)}%";

Debug.Log($"[<color=#A7D147>VideoTXL FFMPEG</color>] Transcode progress '{_ytdlJson.id}': {progressSeconds} {progressPercent}");
}
else
{
if (args.Data.Contains(_ffErrorIdentifier))
{
_ffmpegError += args.Data.Substring(0, args.Data.IndexOf(_ffErrorIdentifier)) + "\n";
}
else
{
_ffmpegError += args.Data + "\n";
}
}
}
};

_ffmpegProcess.Start();
_ffmpegProcess.BeginErrorReadLine();

((MonoBehaviour)videoPlayer).StartCoroutine(URLTranscodeCoroutine(resolvedURL, fullUrlHash, originalUrl, _ffmpegProcess, videoPlayer, urlResolvedCallback, errorCallback));
}
}
else errorCallback(VideoError.Unknown);
#endif
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions Packages/com.texelsaur.video/Runtime/Scripts/SyncPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,14 @@ public override void _SetSourceMode(int mode)
if (!_TakeControl())
return;

#if UNITY_EDITOR_LINUX
if (mode == VideoSource.VIDEO_SOURCE_AVPRO)
{
DebugLog("AVPro does not support Linux!");
mode = VideoSource.VIDEO_SOURCE_UNITY;
}
#endif

_UpdateVideoSourceOverride(mode);
if (mode != VideoSource.VIDEO_SOURCE_NONE)
{
Expand Down Expand Up @@ -1191,6 +1199,7 @@ public void _OnVideoError()

if (Networking.IsOwner(gameObject))
{
#if !UNITY_EDITOR_LINUX
if (shouldFallback)
{
DebugLog("Retrying URL in stream mode");
Expand All @@ -1199,6 +1208,7 @@ public void _OnVideoError()
_PlayVideoAfterFallback(_syncUrl, _syncQuestUrl, _syncUrlSourceIndex, retryTimeout);
return;
}
#endif

if (action == VideoErrorAction.Retry)
_StartVideoLoadDelay(retryTimeout);
Expand Down