diff --git a/contracts/ClientManifest.cs b/contracts/ClientManifest.cs index cbe35e3..9f36791 100644 --- a/contracts/ClientManifest.cs +++ b/contracts/ClientManifest.cs @@ -216,7 +216,9 @@ public class ClientManifest { foreach (var stream in Streams) { - if (stream.Type == track.Type) + var streamName = stream.Name ?? stream.Type.ToString(); + + if (stream.Type == track.Type && streamName == track.TrackName) { for (var i = 0; i < stream.TrackCount; ++i) { diff --git a/contracts/Manifest.cs b/contracts/Manifest.cs index 2500ea6..761d58f 100644 --- a/contracts/Manifest.cs +++ b/contracts/Manifest.cs @@ -52,6 +52,8 @@ public Track(StreamType type) public bool IsMultiFile => string.IsNullOrEmpty(Path.GetExtension(Source)); public uint TrackID => uint.Parse(Parameters.Single(p => p.Name == "trackID").Value); + + public string TrackName => Parameters.SingleOrDefault(p => p.Name == "trackName")?.Value ?? Type.ToString(); } public class VideoTrack : Track diff --git a/pipes/MultiFileStream.cs b/pipes/MultiFileStream.cs index d328acd..f79ca0d 100644 --- a/pipes/MultiFileStream.cs +++ b/pipes/MultiFileStream.cs @@ -16,6 +16,7 @@ public class MultiFileStream private readonly ILogger _logger; private readonly MediaStream _track; private readonly string _trackPrefix; + private readonly bool _isCloseCaption; private readonly StorageEncryptedAssetDecryptionInfo? _decryptInfo; public MultiFileStream( @@ -30,6 +31,8 @@ public MultiFileStream( (_track, _) = manifest.GetStream(track); _trackPrefix = track.Source; _decryptInfo = decryptInfo; + + _isCloseCaption = _track.Type == StreamType.Text && _track.SubType == "SUBT"; } public async Task DownloadAsync(Stream stream, CancellationToken cancellationToken) @@ -38,9 +41,16 @@ public async Task DownloadAsync(Stream stream, CancellationToken cancellationTok try { _logger.LogDebug("Begin downloading track: {name}", _trackPrefix); - chunkName = $"{_trackPrefix}/header"; - var blob = _container.GetBlockBlobClient(chunkName); - await DownloadClearBlobContent(blob, stream, cancellationToken); + + BlockBlobClient blob; + + if (!_isCloseCaption) + { + // Header blob is needed only for audio/video cmaf. + chunkName = $"{_trackPrefix}/header"; + blob = _container.GetBlockBlobClient(chunkName); + await DownloadClearBlobContent(blob, stream, cancellationToken); + } // Report progress every 10%. var i = 0; @@ -133,6 +143,38 @@ private void GenerateCmafFragment(IList inputBoxes, MP4Writer mp4Writer) } } + /// + /// A helper function to generate VTT text for a specific fragblob + /// + /// A list of boxes for an close caption fragblob. + /// The writer for the output stream. + private void GenerateVttContent(IList inputBoxes, MP4Writer mp4Writer) + { + if (inputBoxes.Count != 2) + { + throw new ArgumentException("A live fragment for close caption must contain two mp4-boxes.", nameof(inputBoxes)); + } + + var moofBox = inputBoxes[0] as moofBox; + var mdatBox = inputBoxes[1] as mdatBox; + + if (moofBox == null || mdatBox == null) + { + throw new ArgumentException("A live fragment must contain moof box and mdat box.", nameof(inputBoxes)); + } + + var ttmlText = mdatBox.SampleData; + + byte[] vttText = { 0 }; + + // Call API to convert ttmlText to VTT text. + + // Uncomment this line, it was put here to pass the compiler. + vttText = ttmlText!; + + mp4Writer.Write(vttText); + } + private async Task DownloadClearBlobContent(BlockBlobClient sourceBlob, Stream outputStream, CancellationToken cancellationToken) { using var tmpStream = new MemoryStream(); @@ -177,9 +219,16 @@ private async Task DownloadClearBlobContent(BlockBlobClient sourceBlob, Stream o } else { - // It is for a fragment generated by a live channel. - // Generate cmaf fragment from the input stream. - GenerateCmafFragment(boxes, writer); + if (_isCloseCaption) + { + GenerateVttContent(boxes, writer); + } + else + { + // It is for a fragment generated by a live channel. + // Generate cmaf fragment from the input stream. + GenerateCmafFragment(boxes, writer); + } } writer.Flush(); diff --git a/transform/BasePackager.cs b/transform/BasePackager.cs index 6f960f5..aba658d 100644 --- a/transform/BasePackager.cs +++ b/transform/BasePackager.cs @@ -1,7 +1,6 @@ using AMSMigrate.Contracts; using AMSMigrate.Pipes; using Azure.Storage.Blobs.Specialized; -using FFMpegCore.Pipes; using Microsoft.Extensions.Logging; using System.Diagnostics; @@ -54,7 +53,23 @@ public BasePackager(AssetDetails assetDetails, TransMuxer transMuxer, ILogger lo SelectedTracks = manifest.Tracks.Where(t => { if (t is TextTrack) { - return !TransmuxedDownload && !t.IsMultiFile && (t.Source.EndsWith(VTT_FILE) || t.Parameters.Any(t => t.Name == TRANSCRIPT_SOURCE)); + bool pickThisTextTrack = !TransmuxedDownload && !t.IsMultiFile && (t.Source.EndsWith(VTT_FILE) || t.Parameters.Any(t => t.Name == TRANSCRIPT_SOURCE)); + + if (manifest.IsLiveArchive) + { + pickThisTextTrack = false; + + if (t.IsMultiFile) + { + // Choose the text track with a list of fragblobs for close captions. + pickThisTextTrack = assetDetails.ClientManifest!.Streams.Any( + stream => (stream.Type == StreamType.Text && + stream.SubType == "SUBT") && + stream.Name == t.TrackName); + } + } + + return pickThisTextTrack; } return true; }).ToList(); @@ -66,7 +81,14 @@ public BasePackager(AssetDetails assetDetails, TransMuxer transMuxer, ILogger lo string input; if (track is TextTrack) { - input = track.Source.EndsWith(VTT_FILE) ? track.Source : track.Parameters.Single(p => p.Name == TRANSCRIPT_SOURCE).Value; + if (manifest.IsLiveArchive) + { + input = $"{track.Source}{VTT_FILE}"; + } + else + { + input = track.Source.EndsWith(VTT_FILE) ? track.Source : track.Parameters.Single(p => p.Name == TRANSCRIPT_SOURCE).Value; + } } else { diff --git a/transform/FfmpegPackager.cs b/transform/FfmpegPackager.cs index 76e39d0..2c1f7cb 100644 --- a/transform/FfmpegPackager.cs +++ b/transform/FfmpegPackager.cs @@ -52,7 +52,7 @@ public override async Task RunAsync( { foreach (var track in SelectedTracks) { - var ext = track.IsMultiFile ? MEDIA_FILE : string.Empty; + var ext = track.IsMultiFile ? (track is TextTrack ? VTT_FILE : MEDIA_FILE) : string.Empty; var index = Inputs.IndexOf($"{track.Source}{ext}"); options.SelectStream(0, index, track is VideoTrack ? Channel.Video : Channel.Audio); } diff --git a/transform/ShakaPackager.cs b/transform/ShakaPackager.cs index e3fea17..903cd42 100644 --- a/transform/ShakaPackager.cs +++ b/transform/ShakaPackager.cs @@ -59,7 +59,7 @@ private IEnumerable GetArguments(IList inputs, IList out List arguments = new(SelectedTracks.Select((t, i) => { - var ext = t.IsMultiFile ? MEDIA_FILE : string.Empty; + var ext = t.IsMultiFile ? (t is TextTrack ? VTT_FILE : MEDIA_FILE) : string.Empty; var file = $"{t.Source}{ext}"; var index = Inputs.IndexOf(file); var multiTrack = TransmuxedDownload && FileToTrackMap[file].Count > 1;