Skip to content

Commit

Permalink
feat(toDash): Add support for stable volume/DRC (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
absidue authored May 28, 2024
1 parent 8e942ad commit 031ffb6
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 27 deletions.
12 changes: 11 additions & 1 deletion src/core/mixins/MediaInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,17 @@ export default class MediaInfo {
storyboards = player_response.storyboards;
}

return FormatUtils.toDash(this.streaming_data, this.page[0].video_details?.is_post_live_dvr, url_transformer, format_filter, this.#cpn, this.#actions.session.player, this.#actions, storyboards);
return FormatUtils.toDash(
this.streaming_data,
this.page[0].video_details?.is_post_live_dvr,
url_transformer,
format_filter,
this.#cpn,
this.#actions.session.player,
this.#actions,
storyboards,
options
);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/types/DashOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface DashOptions {
import type { StreamingInfoOptions } from './StreamingInfoOptions.js';

export interface DashOptions extends StreamingInfoOptions {
/**
* Include the storyboards in the DASH manifest when YouTube provides them.
* Not all players support parsing and displaying storyboards.
Expand Down
21 changes: 21 additions & 0 deletions src/types/StreamingInfoOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface StreamingInfoOptions {
/**
* The label to use for the non-DRC streams when a video has DRC and streams.
*
* Defaults to `"Original"`
*/
label_original?: string;
/**
* The label to use for the DRC streams when a video has DRC streams.
*
* Defaults to `"Stable Volume"`
*/
label_drc?: string;
/**
* A function that generates the label to use for the DRC streams when a video has multiple audio tracks and DRC streams.
* The non-DRC streams use the unmodified audio track label provided by YouTube.
*
* Defaults to `(audio_track_display_name) => audio_track_display_name + " (Stable Volume)"`
*/
label_drc_mutiple?: (audio_track_display_name: string) => string;
}
22 changes: 14 additions & 8 deletions src/utils/DashManifest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import type { PlayerStoryboardSpec } from '../parser/nodes.js';
import type { SegmentInfo as FSegmentInfo } from './StreamingInfo.js';
import type { FormatFilter, URLTransformer } from '../types/FormatUtils.js';
import type PlayerLiveStoryboardSpec from '../parser/classes/PlayerLiveStoryboardSpec.js';
import type { StreamingInfoOptions } from '../types/StreamingInfoOptions.js';

interface DashManifestProps {
streamingData: IStreamingData;
isPostLiveDvr: boolean;
transformURL?: URLTransformer;
rejectFormat?: FormatFilter;
options?: StreamingInfoOptions,
cpn?: string;
player?: Player;
actions?: Actions;
Expand Down Expand Up @@ -70,14 +72,15 @@ async function DashManifest({
cpn,
player,
actions,
storyboards
storyboards,
options
}: DashManifestProps) {
const {
getDuration,
audio_sets,
video_sets,
image_sets
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards);
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards, options);

// XXX: DASH spec: https://standards.iso.org/ittf/PubliclyAvailableStandards/c083314_ISO_IEC%2023009-1_2022(en).zip

Expand All @@ -104,11 +107,12 @@ async function DashManifest({
contentType="audio"
>
{
set.track_role &&
<role
schemeIdUri="urn:mpeg:dash:role:2011"
value={set.track_role}
/>
set.track_roles && set.track_roles.map((role) => (
<role
schemeIdUri="urn:mpeg:dash:role:2011"
value={role}
/>
))
}
{
set.track_name &&
Expand Down Expand Up @@ -237,7 +241,8 @@ export function toDash(
cpn?: string,
player?: Player,
actions?: Actions,
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec,
options?: StreamingInfoOptions
) {
if (!streaming_data)
throw new InnertubeError('Streaming data not available');
Expand All @@ -247,6 +252,7 @@ export function toDash(
streamingData={streaming_data}
isPostLiveDvr={is_post_live_dvr}
transformURL={url_transformer}
options={options}
rejectFormat={format_filter}
cpn={cpn}
player={player}
Expand Down
82 changes: 65 additions & 17 deletions src/utils/StreamingInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { IStreamingData } from '../parser/index.js';
import type { Format } from '../parser/misc.js';
import type { PlayerLiveStoryboardSpec } from '../parser/nodes.js';
import type { FormatFilter, URLTransformer } from '../types/FormatUtils.js';
import type { StreamingInfoOptions } from '../types/StreamingInfoOptions.js';

const TAG_ = 'StreamingInfo';

Expand All @@ -27,7 +28,7 @@ export interface AudioSet {
codecs?: string;
audio_sample_rate?: number;
track_name?: string;
track_role?: 'main' | 'dub' | 'description' | 'alternate';
track_roles?: ('main' | 'dub' | 'description' | 'enhanced-audio-intelligibility' | 'alternate')[];
channels?: number;
representations: AudioRepresentation[];
}
Expand Down Expand Up @@ -130,6 +131,12 @@ interface SharedPostLiveDvrInfo {
item?: PostLiveDvrInfo
}

interface DrcLabels {
label_original: string;
label_drc: string;
label_drc_mutiple: (audio_track_display_name: string) => string;
}

function getFormatGroupings(formats: Format[], is_post_live_dvr: boolean) {
const group_info = new Map<string, Format[]>();

Expand All @@ -149,7 +156,9 @@ function getFormatGroupings(formats: Format[], is_post_live_dvr: boolean) {

const audio_track_id = format.audio_track?.id || '';

const group_id = `${mime_type}-${just_codec}-${color_info}-${audio_track_id}`;
const drc = format.is_drc ? 'drc' : '';

const group_id = `${mime_type}-${just_codec}-${color_info}-${audio_track_id}-${drc}`;

if (!group_info.has(group_id)) {
group_info.set(group_id, []);
Expand Down Expand Up @@ -373,8 +382,18 @@ function getAudioRepresentation(
const url = new URL(format.decipher(player));
url.searchParams.set('cpn', cpn || '');

const uid_parts = [ format.itag.toString() ];

if (format.audio_track) {
uid_parts.push(format.audio_track.id);
}

if (format.is_drc) {
uid_parts.push('drc');
}

const rep: AudioRepresentation = {
uid: format.audio_track ? `${format.itag}-${format.audio_track.id}` : format.itag.toString(),
uid: uid_parts.join('-'),
bitrate: format.bitrate,
codecs: !hoisted.includes('codecs') ? getStringBetweenStrings(format.mime_type, 'codecs="', '"') : undefined,
audio_sample_rate: !hoisted.includes('audio_sample_rate') ? format.audio_sample_rate : undefined,
Expand All @@ -385,22 +404,25 @@ function getAudioRepresentation(
return rep;
}

function getTrackRole(format: Format) {
const { audio_track } = format;

if (!audio_track)
function getTrackRoles(format: Format, has_drc_streams: boolean) {
if (!format.audio_track && !has_drc_streams) {
return;
}

if (audio_track.audio_is_default)
return 'main';
const roles: ('main' | 'dub' | 'description' | 'enhanced-audio-intelligibility' | 'alternate')[] = [
format.is_original ? 'main' : 'alternate'
];

if (format.is_dubbed)
return 'dub';
roles.push('dub');

if (format.is_descriptive)
return 'description';
roles.push('description');

if (format.is_drc)
roles.push('enhanced-audio-intelligibility');

return 'alternate';
return roles;
}

function getAudioSet(
Expand All @@ -409,19 +431,34 @@ function getAudioSet(
actions?: Actions,
player?: Player,
cpn?: string,
shared_post_live_dvr_info?: SharedPostLiveDvrInfo
shared_post_live_dvr_info?: SharedPostLiveDvrInfo,
drc_labels?: DrcLabels
) {
const first_format = formats[0];
const { audio_track } = first_format;
const hoisted: string[] = [];

const has_drc_streams = !!drc_labels;

let track_name;

if (audio_track) {
if (has_drc_streams && first_format.is_drc) {
track_name = drc_labels.label_drc_mutiple(audio_track.display_name);
} else {
track_name = audio_track.display_name;
}
} else if (has_drc_streams) {
track_name = first_format.is_drc ? drc_labels.label_drc : drc_labels.label_original;
}

const set: AudioSet = {
mime_type: first_format.mime_type.split(';')[0],
language: first_format.language ?? undefined,
codecs: hoistCodecsIfPossible(formats, hoisted),
audio_sample_rate: hoistNumberAttributeIfPossible(formats, 'audio_sample_rate', hoisted),
track_name: audio_track?.display_name,
track_role: getTrackRole(first_format),
track_name,
track_roles: getTrackRoles(first_format, has_drc_streams),
channels: hoistAudioChannelsIfPossible(formats, hoisted),
representations: formats.map((format) => getAudioRepresentation(format, hoisted, url_transformer, actions, player, cpn, shared_post_live_dvr_info))
};
Expand Down Expand Up @@ -706,7 +743,8 @@ export function getStreamingInfo(
cpn?: string,
player?: Player,
actions?: Actions,
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec,
options?: StreamingInfoOptions
) {
if (!streaming_data)
throw new InnertubeError('Streaming data not available');
Expand Down Expand Up @@ -768,7 +806,17 @@ export function getStreamingInfo(
audio_groups: [] as Format[][]
});

const audio_sets = audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info));
let drc_labels: DrcLabels | undefined;

if (audio_groups.flat().some((format) => format.is_drc)) {
drc_labels = {
label_original: options?.label_original || 'Original',
label_drc: options?.label_drc || 'Stable Volume',
label_drc_mutiple: options?.label_drc_mutiple || ((display_name) => `${display_name} (Stable Volume)`)
};
}

const audio_sets = audio_groups.map((formats) => getAudioSet(formats, url_transformer, actions, player, cpn, shared_post_live_dvr_info, drc_labels));

const video_sets = video_groups.map((formats) => getVideoSet(formats, url_transformer, player, actions, cpn, shared_post_live_dvr_info));

Expand Down

0 comments on commit 031ffb6

Please sign in to comment.