// Appends to `samples'. // Returns nextStartTime. private static ulong ParseFragment(Fragment fragment, IList<MediaSample> samples, MediaStreamType type, ulong chunkStartTime) { // A fragment is a ``chunk'' (with a corresponding <c d=....> in its duration) in the ISM manifest file. TrackFragmentBox traf = fragment.moof.traf; if (traf.tfxd != null) { chunkStartTime = traf.tfxd.FragmentAbsoluteTime; } ulong nextStartTime = 0uL; if (traf.tfrf != null && traf.tfrf.Array.Length > 0u) { nextStartTime = traf.tfrf.Array[0].FragmentAbsoluteTime; } long sampleOffset = fragment.mdat.Start; uint defaultSampleSize = traf.tfhd.default_sample_size; uint sampleSize = defaultSampleSize; uint defaultSampleDuration = traf.tfhd.default_sample_duration; uint duration = defaultSampleDuration; ulong totalDuration = 0; uint sampleCount = traf.trun.sample_count; TrackRunBox.Element[] array = defaultSampleSize == 0u || defaultSampleDuration == 0u ? traf.trun.array : null; for (uint i = 0; i < sampleCount; ++i) { if (defaultSampleSize == 0u) { sampleSize = array[i].sample_size; } if (defaultSampleDuration == 0u) { duration = array[i].sample_duration; } // We add a few dozen MediaSample entries for a chunk. samples.Add(new MediaSample(sampleOffset, (int)sampleSize, chunkStartTime, /*isKeyFrame:*/i == 0 || type == MediaStreamType.Audio)); chunkStartTime += (ulong)duration; totalDuration += (ulong)duration; sampleOffset += sampleSize; } return nextStartTime != 0uL ? nextStartTime : chunkStartTime; }
// Modifies track in place, and appends to mediaSamples. // Returns null on network failure or empty file, otherwise it returns a non-empty array. // The chunk file contents are returned, and are not saved to disk. internal static byte[] DownloadChunk(TrackInfo trackInfo, IList<MediaSample> mediaSamples, ulong chunkStartTime, string manifestParentPath, bool isLive, out ulong nextStartTime) { nextStartTime = 0; // Set even if null is returned. string chunkUrl = trackInfo.Stream.GetChunkUrl(trackInfo.Bitrate, chunkStartTime); // TODO: Move TrackInfo away from Track, keep only fields necessary here, excluding ChunkList. byte[] downloadedBytes; // Will be set below. if (manifestParentPath != null) { // It was a local manifest, so read the chunk from a local file. if (!chunkUrl.StartsWith(LOCAL_URL_PREFIX)) { throw new Exception("ASSERT: Missing local URL prefix."); } // Example chunk URL: "http://local/QualityLevels(900000)/Fragments(video=0)". // TODO: Maybe this needs some further unescaping of %5A etc. (can be tested locally). string chunkDownloadedPath = manifestParentPath + Path.DirectorySeparatorChar + chunkUrl.Substring(LOCAL_URL_PREFIX.Length).Replace('/', Path.DirectorySeparatorChar); using (FileStream fileStream = new FileStream(chunkDownloadedPath, FileMode.Open)) { downloadedBytes = ReadFileStream(fileStream); } if (downloadedBytes.Length == 0) { Console.WriteLine(); Console.WriteLine("Local chunk file empty: " + chunkDownloadedPath); return null; } } else { // Download from the web. WebClient webClient = new WebClient(); try { // TODO: What's the timeout on this? downloadedBytes = webClient.DownloadData(chunkUrl); } catch (WebException) { Thread.Sleep(isLive ? 4000 : 2000); try { downloadedBytes = webClient.DownloadData(chunkUrl); } catch (WebException) { Thread.Sleep(isLive ? 6000 : 3000); try { downloadedBytes = webClient.DownloadData(chunkUrl); } catch (WebException) { // It's an acceptable behavior to stop downloading live streams after 10 seconds. // If it's really live, there should be a new chunk update available every 10 seconds. Console.WriteLine(); Console.WriteLine("Error downloading chunk " + chunkUrl); return null; } } } } if (downloadedBytes.Length == 0) { Console.WriteLine(); Console.WriteLine("Chunk empty: " + chunkUrl); return null; } Fragment fragment = new Fragment(downloadedBytes, 0, downloadedBytes.Length); // This appends to mediaSamples. nextStartTime = ParseFragment(fragment, mediaSamples, trackInfo.Stream.Type, chunkStartTime); if (nextStartTime <= chunkStartTime) { throw new Exception("Found empty chunk."); } return downloadedBytes; }