Skip to content

Commit

Permalink
Disable the NBest Subtitle track for ShakaPackager
Browse files Browse the repository at this point in the history
Description:

  The NBest track wtih .vtt file is not directly used by our AMS origin server today,
  and the cues inside the vtt file might be in wrong order in some cases.

  This change just disable the NBest track when ShakaPackager is used to generate DASH/HLS manifests.

  For LiveToVOD asset, the track with .vtt as its source is treated as NBest track.

  For asset generated by MES Copy job, it may generate multiple text tracks with .cmft file which are generated from the .vtt files,
  In this case, keep one CMFT track for each language,  the one with the biggest vtt file size is used for that language, other close caption tracks are not used for ShakaPackager.

  Even though those vtt files are not used by ShakaPackager, but the cue timestamps are still adjusted to match with new audio/video timelines, and the updated vtt files are copied over to the destination folder
  for the asset owner to consume them with other tools.

  The change keep uses the current design, and extend it by using the settings of the matching Manifiest item in the Manifest list, if the match track is not used
  by the ShakaPackager, the value of Manifest list for that track is set to "", so that the packager knows how to deal with it.

  The change also contains a minor fix where it always uploaded the .vtt files in advance when the asset contains cmft file as track source which are generated from .vtt file,
  since the vtt files are updated and used by ShakaPackager, the pre-uploading should not directly upload vtt files for this asset.
  • Loading branch information
weibz committed Sep 14, 2023
1 parent b2dd1af commit 04ecc23
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 15 deletions.
7 changes: 7 additions & 0 deletions transform/BasePackager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ abstract class BasePackager : IPackager
public const string DASH_MANIFEST = ".mpd";
public const string HLS_MANIFEST = ".m3u8";
public const string VTT_FILE = ".vtt";
public const string CMFT_FILE = ".cmft";
public const string TRANSCRIPT_SOURCE = "transcriptsrc";

protected readonly TransMuxer _transMuxer;
Expand Down Expand Up @@ -214,6 +215,12 @@ await Task.WhenAll(FileToTrackMap.Select(async item =>
}));
}

public virtual void AdjustPackageFiles(string workingDirectory)
{
// Make it no-op in base class for adjusting package files.
// Derived class can override it with its own logic.
return;
}

private async Task DownloadAsync(string workingDirectory, string file, IList<Track> tracks, CancellationToken cancellationToken)
{
Expand Down
9 changes: 7 additions & 2 deletions transform/PackageTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ protected override async Task<string> TransformAsync(
await packager.DownloadInputsAsync(workingDirectory, cancellationToken);
}

// Adjust the package files after the input files are all downloaded.
packager.AdjustPackageFiles(workingDirectory);

var outputFiles = packager.Outputs;
var (outputContainerName, prefix) = outputPath;
var uploadHelper = new UploadHelper(outputContainerName, prefix, _fileUploader);
Expand All @@ -136,7 +139,7 @@ protected override async Task<string> TransformAsync(
}

var manifests = packager.Manifests;
var manifestPaths = manifests.Select(f => Path.Combine(outputDirectory, f)).ToArray();
var manifestPaths = manifests.Select(f => string.IsNullOrEmpty(f) ? "" : Path.Combine(outputDirectory, f)).ToArray();
if (packager.UsePipeForManifests)
{
var manifestPipes = manifestPaths.Select(file => CreateUpload(file, uploadHelper)).ToList();
Expand All @@ -149,7 +152,9 @@ protected override async Task<string> TransformAsync(
}
else
{
uploadPaths.AddRange(manifestPaths);
var manifestsForUpload = manifestPaths.Where(f => f != "");

uploadPaths.AddRange(manifestsForUpload);
}

_logger.LogTrace("Starting static packaging for asset {name}...", assetName);
Expand Down
140 changes: 128 additions & 12 deletions transform/ShakaPackager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using AMSMigrate.Contracts;
using AMSMigrate.Fmp4;
using Azure.ResourceManager.Storage.Models;
using Microsoft.Extensions.Logging;
using System.ComponentModel;
using System.IO;
using System.Text.RegularExpressions;

namespace AMSMigrate.Transform
Expand Down Expand Up @@ -126,18 +129,25 @@ private IEnumerable<string> GetArguments(IList<string> inputs, IList<string> out
var text_tracks = 0;
List<string> arguments = new(SelectedTracks.Select((t, i) =>
{
var source = t.Parameters.SingleOrDefault(p => p.Name == TRANSCRIPT_SOURCE)?.Value ?? t.Source;
var ext = t.IsMultiFile ? (t is TextTrack ? VTT_FILE : MEDIA_FILE) : string.Empty;
var file = $"{source}{ext}";
var index = Inputs.IndexOf(file);
var multiTrack = TransmuxedSmooth && 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},";
var role = t is TextTrack ? $",dash_role={values[text_tracks++ % values.Length].ToString().ToLowerInvariant()}" : string.Empty;
return $"stream={stream},in={inputFile},out={outputs[i]},playlist_name={manifests[i]}{language}{drm_label}{role}";
if (!string.IsNullOrEmpty(manifests[i]))
{
var source = t.Parameters.SingleOrDefault(p => p.Name == TRANSCRIPT_SOURCE)?.Value ?? t.Source;
var ext = t.IsMultiFile ? (t is TextTrack ? VTT_FILE : MEDIA_FILE) : string.Empty;
var file = $"{source}{ext}";
var index = Inputs.IndexOf(file);
var multiTrack = TransmuxedSmooth && 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},";
var role = t is TextTrack ? $",dash_role={values[text_tracks++ % values.Length].ToString().ToLowerInvariant()}" : string.Empty;
return $"stream={stream},in={inputFile},out={outputs[i]},playlist_name={manifests[i]}{language}{drm_label}{role}";
}
else
{
return "";
}
}));
var dash = manifests[manifests.Count - 1];
var hls = manifests[manifests.Count - 2];
Expand Down Expand Up @@ -174,6 +184,112 @@ private IEnumerable<string> GetArguments(IList<string> inputs, IList<string> out
return arguments;
}

/// <summary>
/// Adjust the list of SelectedTracks, Output and Manifests property after all the input files are downloaded/Preprocessed.
/// Give the packager a chance to remove some tracks, especially for SubTitle tracks related to vtt files.
/// </summary>
/// <param name="workingDirectory">The working directory in local machine for packager.</param>
/// <returns></returns>
public override void AdjustPackageFiles(string workingDirectory)
{
// The disableTracks contains a list of tracks that can be disabled for the ShakaPackager.
// each item has the track index in SelectedTracks and the matching vtt file name.
var disabledTracks = new Dictionary<int, string>();

var cmftTracks = new Dictionary<int, string>();
int ti = -1;

// Hold the cmft track which has the largest size of vtt file on each language.
// The key is the language, the value is the pair of track index and the maximum file size of vtt file.
// if the language is not set, use empty string "" as the key.
var largestCmftVtt = new Dictionary<string, Tuple<int, long>>();

foreach (var t in SelectedTracks)
{
ti++;

if (t is TextTrack)
{
if (t.Source.EndsWith(VTT_FILE))
{
// Only NBest close caption track takes .vtt file as source,
// And there must be a single NBest track in an asset.
// This track will be disabled for ShakaPackager.

disabledTracks.Add(ti, t.Source);
}
else if (t.Source.ToLower().EndsWith(CMFT_FILE))
{
var vttSource = t.Parameters.SingleOrDefault(p => p.Name == TRANSCRIPT_SOURCE)?.Value;

if (!string.IsNullOrEmpty(vttSource))
{
string filePath = Path.Combine(workingDirectory, vttSource!);
long length = new FileInfo(filePath).Length;
string lang = t.SystemLanguage ?? "";

if (largestCmftVtt.TryGetValue(lang, out var largeCmft))
{
if (largeCmft.Item2 < length)
{
largestCmftVtt[lang] = new Tuple<int, long>(ti, length);
}
}
else
{
largestCmftVtt.Add(lang, new Tuple<int, long>(ti, length));
}

cmftTracks.Add(ti, vttSource);
}
}
}
}

if (cmftTracks.Count > 0)
{
// There are several text tracks with .cmft files which are generated from vtt source files.
// Hold the track with the largest file size of vtt file for each language.
// disable other text tracks for ShakaPackager.
foreach (var cmft in cmftTracks)
{
Track t = SelectedTracks[cmft.Key];
string lang = t.SystemLanguage ?? "";

if (cmft.Key != largestCmftVtt[lang].Item1)
{
disabledTracks.Add(cmft.Key, cmft.Value);
}
}
}

foreach (var dt in disabledTracks)
{
// For each disabled subtitle track,
// no .m3u8 file is generated for HLS,
// no stream is added for DASH manifest,
// But the vtt file with the adjusted timestamps is still copied over to the destination folder.
// Update the settings in Outputs and Manifests list appropriately.
var si = dt.Key;

Manifests[si] = "";

Outputs[si] = dt.Value;

// Copy over the source vtt files into the output folder.
var src = Path.Combine(workingDirectory, dt.Value!);
var outputDirectory = Path.Combine(workingDirectory, "output");
var dest = Path.Combine(outputDirectory, dt.Value!);

File.Copy(src, dest, true);
}

if (disabledTracks.Count > 0)
{
UsePipeForManifests = false;
}
}

public override Task<bool> RunAsync(
string workingDirectory,
string[] inputs,
Expand Down
26 changes: 25 additions & 1 deletion transform/StorageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,31 @@ public static async Task<IEnumerable<BlockBlobClient>> GetListOfBlobsRemainingAs
CancellationToken cancellationToken)
{
var blobs = await GetListOfBlobsAsync(container, cancellationToken);
return blobs.Where(blob => !manifest.Tracks.Any(t => t.Source == blob.Name));
return blobs.Where(blob => !manifest.Tracks.Any(t => IsValidTrackSource(blob.Name, t)));
}

private static bool IsValidTrackSource(string fileName, Track track)
{
bool validSource = false;

if (track.Source == fileName)
{
validSource = true;
}
else
{
if (track.Source.ToLower().EndsWith(BasePackager.CMFT_FILE))
{
var transcriptsrc = track.Parameters.Single(p => p.Name == BasePackager.TRANSCRIPT_SOURCE);

if (transcriptsrc != null && transcriptsrc.Value == fileName)
{
validSource = true;
}
}
}

return validSource;
}

public static async Task<AssetDetails> GetDetailsAsync(
Expand Down

0 comments on commit 04ecc23

Please sign in to comment.