//----------------------------------------------------------------------------- // Copyright 2015-2018 RenderHeads Ltd. All rights reserverd. //----------------------------------------------------------------------------- #if !UNITY_WSA_10_0 && !UNITY_WINRT_8_1 && !UNITY_WSA && !UNITY_WEBPLAYER #define SUPPORT_SSL #endif using System; using System.IO; using System.Collections.Generic; using UnityEngine; #if SUPPORT_SSL using System.Security.Cryptography.X509Certificates; using System.Net.Security; using System.Net; #endif namespace RenderHeads.Media.AVProVideo { /// /// Utility class to parses an HLS(m3u8) adaptive stream from a URL to /// allow easy inspection and navitation of the stream data /// public class HLSStream : Stream { private const string BANDWITH_NAME = "BANDWIDTH="; private const string RESOLUTION_NAME = "RESOLUTION="; private const string CHUNK_TAG = "#EXTINF"; private const string STREAM_TAG = "#EXT-X-STREAM-INF"; private List _streams; private List _chunks; private string _streamURL; private int _width; private int _height; private int _bandwidth; public override int Width { get { return _width; } } public override int Height { get { return _height; } } public override int Bandwidth { get { return _bandwidth; } } public override string URL { get { return _streamURL; } } public override List GetAllChunks() { List chunks = new List(); for(int i = 0; i < _streams.Count; ++i) { var streamChunks = _streams[i].GetAllChunks(); chunks.AddRange(streamChunks); } chunks.AddRange(_chunks); return chunks; } public override List GetChunks() { return _chunks; } public override List GetAllStreams() { List streams = new List(); for(int i = 0; i < _streams.Count; ++i) { var childrenStreams = _streams[i].GetAllStreams(); streams.AddRange(childrenStreams); } streams.AddRange(_streams); return streams; } public override List GetStreams() { return _streams; } private bool ExtractStreamInfo(string line, ref int width, ref int height, ref int bandwidth) { if (line.StartsWith(STREAM_TAG)) { int bandwidthPos = line.IndexOf(BANDWITH_NAME); if (bandwidthPos >= 0) { int endPos = line.IndexOf(',', bandwidthPos + BANDWITH_NAME.Length); if (endPos < 0) { endPos = line.Length - 1; } if (endPos >= 0 && endPos - BANDWITH_NAME.Length > bandwidthPos) { int length = endPos - bandwidthPos - BANDWITH_NAME.Length; string bandwidthString = line.Substring(bandwidthPos + BANDWITH_NAME.Length, length); if (!int.TryParse(bandwidthString, out bandwidth)) { bandwidth = 0; } } } else { bandwidth = 0; } int resolutionPos = line.IndexOf(RESOLUTION_NAME); if (resolutionPos >= 0) { int endPos = line.IndexOf(',', resolutionPos + RESOLUTION_NAME.Length); if (endPos < 0) { endPos = line.Length - 1; } if (endPos >= 0 && endPos - RESOLUTION_NAME.Length > resolutionPos) { int length = endPos - resolutionPos - RESOLUTION_NAME.Length; string resolutionString = line.Substring(resolutionPos + RESOLUTION_NAME.Length, length); int xPos = resolutionString.IndexOf('x'); if (xPos < 0 || !int.TryParse(resolutionString.Substring(0, xPos), out width) || !int.TryParse(resolutionString.Substring(xPos + 1, resolutionString.Length - (xPos + 1)), out height)) { width = height = 0; } } } else { width = height = 0; } return true; } return false; } private static bool IsChunk(string line) { return line.StartsWith(CHUNK_TAG); } private void ParseFile(string[] text, string path) { bool nextIsChunk = false; bool nextIsStream = false; int width = 0, height = 0, bitrate = 0; for (int i = 0; i < text.Length; ++i) { if (ExtractStreamInfo(text[i], ref width, ref height, ref bitrate)) { nextIsStream = true; nextIsChunk = false; } else if (IsChunk(text[i])) { nextIsChunk = true; nextIsStream = false; } else if (nextIsChunk) { Chunk chunk; chunk.name = path + text[i]; _chunks.Add(chunk); nextIsChunk = false; nextIsStream = false; } else if (nextIsStream) { try { string fullpath = text[i].IndexOf("://") < 0 ? path + text[i] : text[i]; HLSStream stream = new HLSStream(fullpath, width, height, bitrate); _streams.Add(stream); } catch (Exception e) { Debug.LogError("[AVProVideo]HLSParser cannot parse stream " + path + text[i] + ", " + e.Message); } nextIsChunk = false; nextIsStream = false; } else { nextIsChunk = false; nextIsStream = false; } } } public HLSStream(string filename, int width = 0, int height = 0, int bandwidth = 0) { _streams = new List(); _chunks = new List(); _width = width; _height = height; _bandwidth = bandwidth; _streamURL = filename; try { string[] fileLines = null; if (filename.ToLower().StartsWith("http://") || filename.ToLower().StartsWith("https://")) { #if UNITY_WSA_10_0 || UNITY_WINRT_8_1 || UNITY_WSA WWW www = new WWW(filename); int watchdog = 0; while (!www.isDone && watchdog < 5000) { #if NETFX_CORE System.Threading.Tasks.Task.Delay(1).Wait(); #else System.Threading.Thread.Sleep(1); #endif watchdog++; } if (www.isDone && watchdog < 5000) { string fileString = www.text; fileLines = fileString.Split('\n'); } #else #if SUPPORT_SSL if (filename.ToLower().StartsWith("https://")) { ServicePointManager.ServerCertificateValidationCallback = MyRemoteCertificateValidationCallback; } #endif using (System.Net.WebClient webClient = new System.Net.WebClient()) { string fileString = webClient.DownloadString(filename); fileLines = fileString.Split('\n'); } #endif } else { fileLines = File.ReadAllLines(filename); } int lastDash = filename.LastIndexOf('/'); if(lastDash < 0) { lastDash = filename.LastIndexOf('\\'); } string path = _streamURL.Substring(0, lastDash + 1); ParseFile(fileLines, path); } catch (Exception e) { throw e; } } #if SUPPORT_SSL private bool MyRemoteCertificateValidationCallback(System.Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { bool isOk = true; // If there are errors in the certificate chain, // look at each error to determine the cause. if (sslPolicyErrors != SslPolicyErrors.None) { for (int i = 0; i < chain.ChainStatus.Length; i++) { if (chain.ChainStatus[i].Status == X509ChainStatusFlags.RevocationStatusUnknown) { continue; } chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; chain.ChainPolicy.RevocationMode = X509RevocationMode.Online; chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 1, 0); // Note: change flags to X509VerificationFlags.AllFlags to skip all security checks chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; bool chainIsValid = chain.Build((X509Certificate2)certificate); if (!chainIsValid) { isOk = false; break; } } } return isOk; } #endif } }