Skip to content

Commit

Permalink
Add support for smooth streaming asset migration
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
duggaraju committed Aug 9, 2023
1 parent ea28e39 commit 84f32f3
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 121 deletions.
5 changes: 4 additions & 1 deletion ams/AssetMigrationTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ class AssetMigrationResult : MigrationResult
/// </summary>
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)
Expand Down
2 changes: 2 additions & 0 deletions contracts/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions fmp4/Box.cs
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ protected void ReadChildren()
/// </summary>
/// <typeparam name="TChildBoxType">The type of the requested child box.</typeparam>
/// <returns>A box of the requested type.</returns>
public TChildBoxType? GetExactlyOneChildBox<TChildBoxType>() where TChildBoxType : Box
public TChildBoxType GetExactlyOneChildBox<TChildBoxType>() where TChildBoxType : Box
{
IEnumerable<Box> boxes = _children.Where(b => b is TChildBoxType);
int numBoxes = boxes.Count();
Expand All @@ -776,7 +776,7 @@ protected void ReadChildren()
GetType().Name, childBoxName, numBoxes));
}

return boxes.Single() as TChildBoxType;
return (TChildBoxType) boxes.Single();
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion fmp4/FullBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public Byte Version
{
return _version;
}
protected set
set
{
_version = value;
SetDirty();
Expand Down
1 change: 1 addition & 0 deletions fmp4/MP4BoxType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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'


//===================================================================
Expand Down
63 changes: 23 additions & 40 deletions transform/BasePackager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ abstract class BasePackager : IPackager
protected readonly TransMuxer _transMuxer;
protected readonly ILogger _logger;
protected readonly AssetDetails _assetDetails;
private readonly Dictionary<string, IList<Track>> _fileToTrackMap = new Dictionary<string, IList<Track>>();
private readonly SortedDictionary<string, IList<Track>> _fileToTrackMap = new ();

public bool UsePipeForInput { get; protected set; } = false;

Expand Down Expand Up @@ -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<Track> 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<Track> 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));
}));
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion transform/PackageTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> TransformAsync(
Expand Down
11 changes: 8 additions & 3 deletions transform/ShakaPackager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,15 @@ private IEnumerable<string> GetArguments(IList<string> inputs, IList<string> out
List<string> 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];
Expand Down
123 changes: 50 additions & 73 deletions transform/TransMuxer.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -13,7 +14,6 @@ internal class TransMuxer

private readonly MigratorOptions _options;
private readonly ILogger _logger;
private string? _hostName = null;

public TransMuxer(
MigratorOptions options,
Expand All @@ -23,85 +23,23 @@ public TransMuxer(
_options = options;
}

public async Task<Uri?> 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<string> GetHostNameAsync(AssetRecord record, CancellationToken cancellationToken)
{
if (_hostName == null)
{
_hostName = await record.Account.GetStreamingEndpointAsync(cancellationToken: cancellationToken);
}
return _hostName;
}

public async Task<IMediaAnalysis> 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 =>
Expand All @@ -119,7 +57,46 @@ await FFMpegArguments
.ForceFormat("mp4")
.WithCustomArgument("-movflags faststart");
}
}).ProcessAsynchronously(throwOnError: true);
});
await RunAsync(processor, cancellationToken);
}

/// <summary>
/// Transmux smooth input and filter by track id.
/// </summary>
/// <param name="trackId">The track id to filter by.</param>
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<trafBox>();
var tfhd = traf.GetExactlyOneChildBox<tfhdBox>();
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<trunBox>();
trun.Version = (byte)1;
moof.WriteTo(writer);
}
else if (box.Type == MP4BoxType.mfra)
{
break;
}
else if (!skip)
{
box.WriteTo(writer);
}
}
}
}
}

0 comments on commit 84f32f3

Please sign in to comment.