From 84f32f35fdea71ae73f0afa6a27951af15ef44e5 Mon Sep 17 00:00:00 2001 From: Prakash Duggaraju Date: Tue, 8 Aug 2023 18:13:32 -0700 Subject: [PATCH] Add support for smooth streaming asset migration Use the MP4 parser to transmux smooth to a format that shaka can accept. Always set 'trun' box version to 1 to support signed CTS Filter by track id since shaka fails on single track moof. --- ams/AssetMigrationTracker.cs | 5 +- contracts/Manifest.cs | 2 + fmp4/Box.cs | 4 +- fmp4/FullBox.cs | 2 +- fmp4/MP4BoxType.cs | 1 + transform/BasePackager.cs | 63 +++++++---------- transform/PackageTransform.cs | 4 +- transform/ShakaPackager.cs | 11 ++- transform/TransMuxer.cs | 123 ++++++++++++++-------------------- 9 files changed, 94 insertions(+), 121 deletions(-) diff --git a/ams/AssetMigrationTracker.cs b/ams/AssetMigrationTracker.cs index 601dbf1..00efa0f 100644 --- a/ams/AssetMigrationTracker.cs +++ b/ams/AssetMigrationTracker.cs @@ -48,7 +48,10 @@ class AssetMigrationResult : MigrationResult /// public bool IsSupportedAsset(bool enableLiveAsset) { - return (AssetType != null && (AssetType == AssetType_NonIsm || AssetType.StartsWith("mp4") || (enableLiveAsset && AssetType == "vod-fmp4"))); + return (AssetType != null && (AssetType == AssetType_NonIsm + || AssetType == "fmp4" + || AssetType.StartsWith("mp4") + || (enableLiveAsset && AssetType == "vod-fmp4"))); } public AssetMigrationResult(MigrationStatus status = MigrationStatus.NotMigrated, Uri? outputPath = null, string? assetType = null, string? manifestName = null) : base(status) diff --git a/contracts/Manifest.cs b/contracts/Manifest.cs index 36f0340..2500ea6 100644 --- a/contracts/Manifest.cs +++ b/contracts/Manifest.cs @@ -50,6 +50,8 @@ public Track(StreamType type) // Check if the track is stored as one file per fragment. public bool IsMultiFile => string.IsNullOrEmpty(Path.GetExtension(Source)); + + public uint TrackID => uint.Parse(Parameters.Single(p => p.Name == "trackID").Value); } public class VideoTrack : Track diff --git a/fmp4/Box.cs b/fmp4/Box.cs index 873a7ae..5d5a03d 100644 --- a/fmp4/Box.cs +++ b/fmp4/Box.cs @@ -752,7 +752,7 @@ protected void ReadChildren() /// /// The type of the requested child box. /// A box of the requested type. - public TChildBoxType? GetExactlyOneChildBox() where TChildBoxType : Box + public TChildBoxType GetExactlyOneChildBox() where TChildBoxType : Box { IEnumerable boxes = _children.Where(b => b is TChildBoxType); int numBoxes = boxes.Count(); @@ -776,7 +776,7 @@ protected void ReadChildren() GetType().Name, childBoxName, numBoxes)); } - return boxes.Single() as TChildBoxType; + return (TChildBoxType) boxes.Single(); } /// diff --git a/fmp4/FullBox.cs b/fmp4/FullBox.cs index 9ad4ef6..da93d9d 100644 --- a/fmp4/FullBox.cs +++ b/fmp4/FullBox.cs @@ -126,7 +126,7 @@ public Byte Version { return _version; } - protected set + set { _version = value; SetDirty(); diff --git a/fmp4/MP4BoxType.cs b/fmp4/MP4BoxType.cs index dd09c9e..9c9067d 100644 --- a/fmp4/MP4BoxType.cs +++ b/fmp4/MP4BoxType.cs @@ -31,6 +31,7 @@ public static class MP4BoxType public const UInt32 mdia = 0x6D646961; // 'mdia' public const UInt32 hdlr = 0x68646C72; // 'hdlr' public const UInt32 mdhd = 0x6D646864; // 'mdhd' + public const UInt32 mfra = 0x6D666661; // 'mfra' //=================================================================== diff --git a/transform/BasePackager.cs b/transform/BasePackager.cs index 23ff28e..6f960f5 100644 --- a/transform/BasePackager.cs +++ b/transform/BasePackager.cs @@ -18,7 +18,7 @@ abstract class BasePackager : IPackager protected readonly TransMuxer _transMuxer; protected readonly ILogger _logger; protected readonly AssetDetails _assetDetails; - private readonly Dictionary> _fileToTrackMap = new Dictionary>(); + private readonly SortedDictionary> _fileToTrackMap = new (); public bool UsePipeForInput { get; protected set; } = false; @@ -172,63 +172,46 @@ public Pipe[] GetInputPipes(string workingDirectory) public async Task DownloadInputsAsync(string workingDirectory, CancellationToken cancellationToken) { - if (_assetDetails.ClientManifest != null && - _assetDetails.ClientManifest.HasDiscontinuities(_logger) && - _assetDetails is AssetRecord assetRecord) + await Task.WhenAll(FileToTrackMap.Select(async item => { - await StreamingTransMuxAsync(workingDirectory, assetRecord, cancellationToken); - } - else - { - await Task.WhenAll(FileToTrackMap.Select(async item => - { - var (file, tracks) = item; - var filePath = Path.Combine(workingDirectory, file); - await DownloadAsync(filePath, tracks, cancellationToken); - })); - } + var (file, tracks) = item; + await DownloadAsync(workingDirectory, file, tracks, cancellationToken); + })); } - public async Task StreamingTransMuxAsync(string workingDirectory, AssetRecord assetRecord, CancellationToken cancellationToken) + private async Task DownloadAsync(string workingDirectory, string file, IList tracks, CancellationToken cancellationToken) { - Debug.Assert(Inputs.Count == 1); - var uri = await _transMuxer.GetStreamingUrlAsync(assetRecord, cancellationToken); - if (uri == null) + var tempDirectory = Path.Combine(workingDirectory, "input"); + if (TransmuxedDownload) { - _logger.LogWarning("No streaming locator found for asset {name}", assetRecord.AssetName); - throw new NotImplementedException("Failed to get locator"); + Directory.CreateDirectory(tempDirectory); } - var filePath = Path.Combine(workingDirectory, Inputs[0]); - await _transMuxer.TransmuxUriAsync(uri, filePath, cancellationToken); - } - - private async Task DownloadAsync(string filePath, IList tracks, CancellationToken cancellationToken) - { - IPipeSource source; + var filePath = Path.Combine(TransmuxedDownload ? tempDirectory : workingDirectory, file); + if (tracks.Count == 1 && tracks[0].IsMultiFile) { var track = tracks[0]; var multiFileStream = new MultiFileStream(_assetDetails.Container, track, _assetDetails.ClientManifest!, _assetDetails.DecryptInfo, _logger); - source = new MultiFilePipe(filePath, multiFileStream); + var source = new MultiFilePipe(file, multiFileStream); + await source.DownloadAsync(filePath, cancellationToken); } else { - var blob = _assetDetails.Container.GetBlockBlobClient(Path.GetFileName(filePath)); - source = new BlobSource(blob, _assetDetails.DecryptInfo, _logger); + var blob = _assetDetails.Container.GetBlockBlobClient(file); + var source = new BlobSource(blob, _assetDetails.DecryptInfo, _logger); + await source.DownloadAsync(filePath, cancellationToken); } if (TransmuxedDownload) { - await _transMuxer.TransMuxAsync(source, filePath, cancellationToken); - } - else if (source is MultiFilePipe pipe) - { - await pipe.DownloadAsync(filePath, cancellationToken); - } - else if (source is BlobSource blobSource) - { - await blobSource.DownloadAsync(filePath, cancellationToken); + await Task.WhenAll(tracks.Select(async track => + { + using var sourceFile = File.OpenRead(filePath); + var filename = tracks.Count == 1 ? file : $"{Path.GetFileNameWithoutExtension(file)}_{track.TrackID}{Path.GetExtension(file)}"; + using var destFile = File.OpenWrite(Path.Combine(workingDirectory, filename)); + await Task.Run(() => _transMuxer.TransmuxSmooth(sourceFile, destFile, track.TrackID)); + })); } } } diff --git a/transform/PackageTransform.cs b/transform/PackageTransform.cs index 31ade1a..f7e00b2 100644 --- a/transform/PackageTransform.cs +++ b/transform/PackageTransform.cs @@ -35,7 +35,9 @@ protected override bool IsSupported(AssetDetails details) { return false; } - return details.Manifest.Format.StartsWith("mp4") || (_globalOptions.EnableLiveAsset && details.Manifest.Format == "vod-fmp4"); + return details.Manifest.Format.StartsWith("mp4") + || details.Manifest.Format.Equals("fmp4") + || (_globalOptions.EnableLiveAsset && details.Manifest.Format == "vod-fmp4"); } protected override async Task TransformAsync( diff --git a/transform/ShakaPackager.cs b/transform/ShakaPackager.cs index c3d979e..e3fea17 100644 --- a/transform/ShakaPackager.cs +++ b/transform/ShakaPackager.cs @@ -60,10 +60,15 @@ private IEnumerable GetArguments(IList inputs, IList out List arguments = new(SelectedTracks.Select((t, i) => { var ext = t.IsMultiFile ? MEDIA_FILE : string.Empty; - var index = Inputs.Count == 1 ? 0 : Inputs.IndexOf($"{t.Source}{ext}"); - var stream = Inputs.Count == 1? i.ToString(): t.Type.ToString().ToLowerInvariant(); + var file = $"{t.Source}{ext}"; + var index = Inputs.IndexOf(file); + var multiTrack = TransmuxedDownload && FileToTrackMap[file].Count > 1; + var inputFile = multiTrack ? + Path.Combine(Path.GetDirectoryName(inputs[index])!, $"{Path.GetFileNameWithoutExtension(file)}_{t.TrackID}{Path.GetExtension(file)}") : + inputs[index]; + var stream = t.Type.ToString().ToLowerInvariant(); var language = string.IsNullOrEmpty(t.SystemLanguage) || t.SystemLanguage == "und" ? string.Empty : $"language={t.SystemLanguage},"; - return $"stream={stream},in={inputs[index]},out={outputs[i]},{language}playlist_name={manifests[i]}{drm_label}"; + return $"stream={stream},in={inputFile},out={outputs[i]},{language}playlist_name={manifests[i]}{drm_label}"; })); var dash = manifests[manifests.Count - 1]; var hls = manifests[manifests.Count - 2]; diff --git a/transform/TransMuxer.cs b/transform/TransMuxer.cs index 5193a57..c8fdfe5 100644 --- a/transform/TransMuxer.cs +++ b/transform/TransMuxer.cs @@ -1,9 +1,10 @@ -using AMSMigrate.Ams; -using AMSMigrate.Contracts; +using AMSMigrate.Contracts; +using AMSMigrate.Fmp4; using Azure.ResourceManager.Media.Models; using FFMpegCore; using FFMpegCore.Pipes; using Microsoft.Extensions.Logging; +using System.Text; namespace AMSMigrate.Transform { @@ -13,7 +14,6 @@ internal class TransMuxer private readonly MigratorOptions _options; private readonly ILogger _logger; - private string? _hostName = null; public TransMuxer( MigratorOptions options, @@ -23,85 +23,23 @@ public TransMuxer( _options = options; } - public async Task GetStreamingUrlAsync(AssetRecord record, CancellationToken cancellationToken) - { - var hostName = await GetHostNameAsync(record, cancellationToken); - var locator = await record.Account.GetStreamingLocatorAsync(record.Asset, cancellationToken); - if (locator == null) - { - return null; - } - - StreamingPathsResult pathResult = await locator.GetStreamingPathsAsync(cancellationToken); - var path = pathResult.StreamingPaths.SingleOrDefault(p => p.StreamingProtocol == Protocol); - if (path == null) - { - _logger.LogWarning("The locator {locator} has no HLS streaming support.", locator.Id); - return null; - } - var uri = new UriBuilder("https", _hostName) - { - Path = path.Paths[0] + ".m3u8" - }.Uri; - return uri; - } - - private async Task GetHostNameAsync(AssetRecord record, CancellationToken cancellationToken) - { - if (_hostName == null) - { - _hostName = await record.Account.GetStreamingEndpointAsync(cancellationToken: cancellationToken); - } - return _hostName; - } - public async Task AnalyzeAsync(Uri uri, CancellationToken cancellationToken) { return await FFProbe.AnalyseAsync(uri, null, cancellationToken); } - public async Task TransmuxUriAsync(Uri uri, FFMpegCore.MediaStream stream, string filePath, CancellationToken cancellationToken) - { - var processor = FFMpegArguments.FromUrlInput(uri) - .OutputToFile(filePath, overwrite: true, options => - { - options.CopyChannel() - .SelectStream(stream.Index, 0) - .WithCustomArgument("-movflags faststart"); - }); - _logger.LogInformation("Running ffmpeg {args}", processor.Arguments); - await processor - .CancellableThrough(cancellationToken) - .ProcessAsynchronously(true); - } - - public async Task TransmuxUriAsync(Uri uri, string filePath, CancellationToken cancellationToken) + private async Task RunAsync(FFMpegArgumentProcessor processor, CancellationToken cancellationToken) { - var result = await FFProbe.AnalyseAsync(uri, null, cancellationToken); - var processor = FFMpegArguments.FromUrlInput(uri) - .OutputToFile(filePath, overwrite: true, options => - { - foreach (var stream in result.AudioStreams) - { - options.SelectStream(stream.Index, 0); - } - foreach (var stream in result.VideoStreams) - { - options.SelectStream(stream.Index, 0); - } - - options.CopyChannel() - .WithCustomArgument("-movflags faststart"); - }); - _logger.LogDebug("Running ffmpeg {args}", processor.Arguments); - await processor - .CancellableThrough(cancellationToken) - .ProcessAsynchronously(true); + _logger.LogDebug(Events.Ffmpeg, "Running ffmpeg {args}", processor.Arguments); + await processor.CancellableThrough(cancellationToken) + .NotifyOnError(line => _logger.LogTrace(Events.Ffmpeg, line)) + .NotifyOnOutput(line => _logger.LogTrace(Events.Ffmpeg, line)) + .ProcessAsynchronously(throwOnError: true); } public async Task TransMuxAsync(IPipeSource source, string destination, CancellationToken cancellationToken) { - await FFMpegArguments + var processor = FFMpegArguments .FromPipeInput(source) //.WithGlobalOptions(options => options.WithVerbosityLevel(FFMpegCore.Arguments.VerbosityLevel.Verbose)) .OutputToFile(destination, overwrite: false, options => @@ -119,7 +57,46 @@ await FFMpegArguments .ForceFormat("mp4") .WithCustomArgument("-movflags faststart"); } - }).ProcessAsynchronously(throwOnError: true); + }); + await RunAsync(processor, cancellationToken); + } + + /// + /// Transmux smooth input and filter by track id. + /// + /// The track id to filter by. + public void TransmuxSmooth(Stream source, Stream destination, uint trackId) + { + using var reader = new MP4Reader(source, Encoding.UTF8, leaveOpen: true); + using var writer = new MP4Writer(destination, Encoding.UTF8, leaveOpen: true); + bool skip = false; + while(source.Position < source.Length) + { + var box = MP4BoxFactory.ParseSingleBox(reader); + if (box is moofBox moof) + { + var traf = moof.GetExactlyOneChildBox(); + var tfhd = traf.GetExactlyOneChildBox(); + skip = tfhd.TrackId != trackId; + if (skip) + { + continue; + } + + // Expression Encoder sets version to 0 even for signed CTS. Always set version to 1 + var trun = traf.GetExactlyOneChildBox(); + trun.Version = (byte)1; + moof.WriteTo(writer); + } + else if (box.Type == MP4BoxType.mfra) + { + break; + } + else if (!skip) + { + box.WriteTo(writer); + } + } } } }