Skip to content

Instantly share code, notes, and snippets.

@mminer
Created January 28, 2021 00:58
Show Gist options
  • Save mminer/43d86530a9479a3b5c725f7f118b64be to your computer and use it in GitHub Desktop.
Save mminer/43d86530a9479a3b5c725f7f118b64be to your computer and use it in GitHub Desktop.
YouTube video player for Unity.
using System;
using System.Text.RegularExpressions;
using UnityEngine;
/// <summary>
/// Holds the result of parsing the ytInitialPlayerResponse JSON from a YouTube page.
/// </summary>
/// <remarks>
/// This is an incomplete list of fields in ytInitialPlayerResponse.
/// The full object contains many more, but we only care about a few.
/// </remarks>
[Serializable]
public struct YouTubePlayerResponse
{
[Serializable]
public struct PlayabilityStatus
{
public string reason;
public string status;
}
[Serializable]
public struct StreamingData
{
[Serializable]
public struct Format
{
public int bitrate;
public string fps;
public string mimeType;
public string quality;
public string url;
/// <summary>
/// Whether this format is compatible with Unity's VideoPlayer.
/// </summary>
// TODO: this is probably needlessly restrictive
public bool IsCompatible => mimeType.Contains("video/mp4");
}
public Format[] formats;
}
[Serializable]
public struct VideoDetails
{
public string shortDescription;
public string title;
}
public PlayabilityStatus playabilityStatus;
public StreamingData streamingData;
public VideoDetails videoDetails;
// Example of unplayable video: https://www.youtube.com/watch?v=qm5q1o7ofnc
public bool IsPlayable => playabilityStatus.status != "ERROR";
public static YouTubePlayerResponse FromJson(string json)
{
return JsonUtility.FromJson<YouTubePlayerResponse>(json);
}
public static YouTubePlayerResponse? FromPageSource(string pageSource)
{
// Extract the JSON from the JavaScript in the HTML.
var regex = new Regex(@"ytInitialPlayerResponse\s*=\s*(\{.+?\})\s*;", RegexOptions.Multiline);
var match = regex.Match(pageSource);
if (!match.Success)
{
return null;
}
var json = match.Result("$1");
return FromJson(json);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
public enum YouTubeRequestResult
{
InProgress,
Error,
Success,
}
/// <summary>
/// Provides a method to find URLs to a YouTube video's raw video files.
/// </summary>
public class YouTubeRequest
{
public string Error { get; private set; }
public YouTubeRequestResult Result { get; private set; }
public YouTubePlayerResponse.StreamingData.Format BestQualityFormat => Formats
.OrderByDescending(format => format.bitrate)
.First();
public List<YouTubePlayerResponse.StreamingData.Format> Formats => _playerResponse.streamingData.formats
.Where(format => format.IsCompatible)
.ToList();
private YouTubePlayerResponse _playerResponse;
private readonly string _youTubeUrl;
/// <summary>
/// Creates a request to find a YouTube video files.
/// </summary>
/// <param name="youTubeUrl">YouTube video URL.</param>
public YouTubeRequest(string youTubeUrl)
{
if (!YouTubeUtils.TryNormalizeYouTubeUrl(youTubeUrl, out _youTubeUrl))
{
throw new ArgumentException("Invalid YouTube URL.", nameof(youTubeUrl));
}
}
/// <summary>
/// Downloads a list of video files for a YouTube video.
/// </summary>
public IEnumerator SendRequest()
{
Result = YouTubeRequestResult.InProgress;
Debug.Log($"Fetching YouTube page source from {_youTubeUrl}");
// Fetch the page source. That is, get the HTML that the browser downloads when you visit a YouTube page.
string pageSource;
using (var request = UnityWebRequest.Get(_youTubeUrl))
{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
ReportError($"Error fetching YouTube page: {request.error}");
yield break;
}
pageSource = request.downloadHandler.text;
}
// Extract video details from the page HTML.
var playerResponse = YouTubePlayerResponse.FromPageSource(pageSource);
if (!playerResponse.HasValue)
{
ReportError("Unable to parse video details from YouTube page.");
yield break;
}
_playerResponse = playerResponse.Value;
Debug.Log($"Downloaded details for YouTube video: \"{playerResponse.Value.videoDetails.title}\"");
if (!playerResponse.Value.IsPlayable)
{
ReportError($"YouTube video unplayable: {playerResponse.Value.playabilityStatus.reason}");
yield break;
}
Result = YouTubeRequestResult.Success;
}
private void ReportError(string error)
{
Error = error;
Result = YouTubeRequestResult.Error;
}
}
using System;
using System.Web;
/// <summary>
/// Utility functions for interacting with YouTube.
/// </summary>
public static class YouTubeUtils
{
/// <summary>
/// Pulls the video ID from a YouTube URL.
/// </summary>
/// <param name="youTubeUrl">YouTube URL to extract ID from.</param>
/// <returns>Canonical YouTube video ID (e.g. VZBYoN-iHkE).</returns>
public static string ExtractVideoId(string youTubeUrl)
{
if (string.IsNullOrEmpty(youTubeUrl))
{
return null;
}
// YouTube URLs come in a few different formats.
// Normalize the URL such that the video ID appears in the query string.
youTubeUrl = youTubeUrl
.Trim()
.Replace("youtu.be/", "youtube.com/watch?v=")
.Replace("youtube.com/embed/", "youtube.com/watch?v=")
.Replace("/watch#", "/watch?");
if (youTubeUrl.Contains("/v/"))
{
var absolutePath = new Uri(youTubeUrl).AbsolutePath;
absolutePath = absolutePath.Replace("/v/", "/watch?v=");
youTubeUrl = $"https://youtube.com{absolutePath}";
}
// The URL should now contain a query string of the format v={video-id}.
var queryString = new Uri(youTubeUrl).Query;
var query = HttpUtility.ParseQueryString(queryString);
return query.Get("v");
}
/// <summary>
/// Normalizes a YouTube URL to the format https://youtube.com/watch?v={video-id}.
/// </summary>
/// <param name="youTubeUrl">YouTube URL to normalize.</param>
/// <param name="normalizedYouTubeUrl">Normalized YouTube URL.</param>
/// <returns>Whether normalization was successful and the URL is valid.</returns>
public static bool TryNormalizeYouTubeUrl(string youTubeUrl, out string normalizedYouTubeUrl)
{
var videoId = ExtractVideoId(youTubeUrl);
if (string.IsNullOrEmpty(videoId))
{
normalizedYouTubeUrl = null;
return false;
}
normalizedYouTubeUrl = $"https://www.youtube.com/watch?v={videoId}&gl=US&hl=en&has_verified=1&bpctr=9999999999";
return true;
}
}
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Video;
public class YouTubeVideoPlayer : MonoBehaviour
{
public VideoPlayer videoPlayer;
[InspectorName("YouTube URL")]
public string youTubeUrl;
private void Awake()
{
if (videoPlayer == null)
{
videoPlayer = GetComponent<VideoPlayer>();
}
}
private void Start()
{
StartCoroutine(SetVideoPlayerUrl());
}
private IEnumerator SetVideoPlayerUrl()
{
if (videoPlayer == null)
{
Debug.LogError("No video player.");
yield break;
}
var request = new YouTubeRequest(youTubeUrl);
yield return request.SendRequest();
if (request.Result == YouTubeRequestResult.Error)
{
Debug.LogError($"Failed to fetch YouTube video details: {request.Error}");
yield break;
}
Debug.Log("Fetched YouTube formats.");
try
{
videoPlayer.url = request.BestQualityFormat.url;
}
catch (InvalidOperationException)
{
Debug.LogError("Failed to find any compatible formats.");
}
}
}
@mminer
Copy link
Author

mminer commented Feb 28, 2022

@aphaena: It’s tough to say what the problem might be. It might be helpful to print the JSON string that YouTubePlayerResponse:FromPageSource returns, or what the final URL that it sets on videoPlayer.url is.

@aphaena
Copy link

aphaena commented Mar 1, 2022

It's perfect when i active my gameobject with youtube video player after my app is started.
Thank's you for your share
Beautifull job :)

@RomainBitard
Copy link

I don't know about you all but for me highest quality lags a lot even with a fiber connection on PC. But the code seems to work !

@mpalazli
Copy link

hello the video starts after 9-10 seconds how can i make that this time to 1-2 seconds
Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment