Skip to content

Commit

Permalink
feat(toDash)!: Add support for generating manifests for Post Live DVR…
Browse files Browse the repository at this point in the history
… videos (#580)

BREAKING CHANGES: The `duration` property in `StreamingInfo` has been
replaced by the asynchronous `getDuration()` function, as getting the duration
of Post Live DVR videos requires making a fetch request.
  • Loading branch information
absidue authored Jan 18, 2024
1 parent 2073aa9 commit 6dd03e1
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 51 deletions.
12 changes: 6 additions & 6 deletions src/core/mixins/MediaInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type Format from '../../parser/classes/misc/Format.js';
import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.js';
import { Parser } from '../../parser/index.js';
import type { DashOptions } from '../../types/DashOptions.js';
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.js';
import { getStreamingInfo } from '../../utils/StreamingInfo.js';
import ContinuationItem from '../../parser/classes/ContinuationItem.js';
import TranscriptInfo from '../../parser/youtube/TranscriptInfo.js';
Expand Down Expand Up @@ -50,17 +49,17 @@ export default class MediaInfo {
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter, options: DashOptions = { include_thumbnails: false }): Promise<string> {
const player_response = this.#page[0];

if (player_response.video_details && (player_response.video_details.is_live || player_response.video_details.is_post_live_dvr)) {
throw new InnertubeError('Generating DASH manifests for live and Post-Live-DVR videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
if (player_response.video_details && (player_response.video_details.is_live)) {
throw new InnertubeError('Generating DASH manifests for live videos is not supported. Please use the DASH and HLS manifests provided by YouTube in `streaming_data.dash_manifest_url` and `streaming_data.hls_manifest_url` instead.');
}

let storyboards;

if (options.include_thumbnails && player_response.storyboards?.is(PlayerStoryboardSpec)) {
if (options.include_thumbnails && player_response.storyboards) {
storyboards = player_response.storyboards;
}

return FormatUtils.toDash(this.streaming_data, 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);
}

/**
Expand All @@ -69,12 +68,13 @@ export default class MediaInfo {
getStreamingInfo(url_transformer?: URLTransformer, format_filter?: FormatFilter) {
return getStreamingInfo(
this.streaming_data,
this.page[0].video_details?.is_post_live_dvr,
url_transformer,
format_filter,
this.cpn,
this.#actions.session.player,
this.#actions,
this.#page[0].storyboards?.is(PlayerStoryboardSpec) ? this.#page[0].storyboards : undefined
this.#page[0].storyboards ? this.#page[0].storyboards : undefined
);
}

Expand Down
27 changes: 24 additions & 3 deletions src/parser/classes/PlayerLiveStoryboardSpec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';

export interface LiveStoryboardData {
type: 'live',
template_url: string,
thumbnail_width: number,
thumbnail_height: number,
columns: number,
rows: number
}

export default class PlayerLiveStoryboardSpec extends YTNode {
static type = 'PlayerLiveStoryboardSpec';

constructor() {
board: LiveStoryboardData;

constructor(data: RawNode) {
super();
// TODO: A little bit different from PlayerLiveStoryboardSpec
// https://i.ytimg.com/sb/5qap5aO4i9A/storyboard_live_90_2x2_b2/M$M.jpg?rs=AOn4CLC9s6IeOsw_gKvEbsbU9y-e2FVRTw#159#90#2#2

const [ template_url, thumbnail_width, thumbnail_height, columns, rows ] = data.spec.split('#');

this.board = {
type: 'live',
template_url,
thumbnail_width: parseInt(thumbnail_width, 10),
thumbnail_height: parseInt(thumbnail_height, 10),
columns: parseInt(columns, 10),
rows: parseInt(rows, 10)
};
}
}
2 changes: 2 additions & 0 deletions src/parser/classes/PlayerStoryboardSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { YTNode } from '../helpers.js';
import type { RawNode } from '../index.js';

export interface StoryboardData {
type: 'vod'
template_url: string;
thumbnail_width: number;
thumbnail_height: number;
Expand Down Expand Up @@ -31,6 +32,7 @@ export default class PlayerStoryboardSpec extends YTNode {
const storyboard_count = Math.ceil(parseInt(thumbnail_count, 10) / (parseInt(columns, 10) * parseInt(rows, 10)));

return {
type: 'vod',
template_url: url.toString().replace('$L', i).replace('$N', name),
thumbnail_width: parseInt(thumbnail_width, 10),
thumbnail_height: parseInt(thumbnail_height, 10),
Expand Down
28 changes: 16 additions & 12 deletions src/utils/DashManifest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type Actions from '../core/Actions.js';
import type Player from '../core/Player.js';
import type { IStreamingData } from '../parser/index.js';
import type { PlayerStoryboardSpec } from '../parser/nodes.js';
import type { PlayerLiveStoryboardSpec, PlayerStoryboardSpec } from '../parser/nodes.js';
import * as DashUtils from './DashUtils.js';
import type { SegmentInfo as FSegmentInfo } from './StreamingInfo.js';
import { getStreamingInfo } from './StreamingInfo.js';
Expand All @@ -13,21 +13,22 @@ import { InnertubeError } from './Utils.js';

interface DashManifestProps {
streamingData: IStreamingData;
isPostLiveDvr: boolean;
transformURL?: URLTransformer;
rejectFormat?: FormatFilter;
cpn?: string;
player?: Player;
actions?: Actions;
storyboards?: PlayerStoryboardSpec;
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
}

async function OTFSegmentInfo({ info }: { info: FSegmentInfo }) {
if (!info.is_oft) return null;
async function OTFPostLiveDvrSegmentInfo({ info }: { info: FSegmentInfo }) {
if (!info.is_oft && !info.is_post_live_dvr) return null;

const template = await info.getSegmentTemplate();

return <segment-template
startNumber="1"
startNumber={template.init_url ? '1' : '0'}
timescale="1000"
initialization={template.init_url}
media={template.media_url}
Expand All @@ -46,8 +47,8 @@ async function OTFSegmentInfo({ info }: { info: FSegmentInfo }) {
}

function SegmentInfo({ info }: { info: FSegmentInfo }) {
if (info.is_oft) {
return <OTFSegmentInfo info={info} />;
if (info.is_oft || info.is_post_live_dvr) {
return <OTFPostLiveDvrSegmentInfo info={info} />;
}
return <>
<base-url>
Expand All @@ -59,8 +60,9 @@ function SegmentInfo({ info }: { info: FSegmentInfo }) {
</>;
}

function DashManifest({
async function DashManifest({
streamingData,
isPostLiveDvr,
transformURL,
rejectFormat,
cpn,
Expand All @@ -69,11 +71,11 @@ function DashManifest({
storyboards
}: DashManifestProps) {
const {
duration,
getDuration,
audio_sets,
video_sets,
image_sets
} = getStreamingInfo(streamingData, transformURL, rejectFormat, cpn, player, actions, storyboards);
} = getStreamingInfo(streamingData, isPostLiveDvr, transformURL, rejectFormat, cpn, player, actions, storyboards);

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

Expand All @@ -82,7 +84,7 @@ function DashManifest({
minBufferTime="PT1.500S"
profiles="urn:mpeg:dash:profile:isoff-main:2011"
type="static"
mediaPresentationDuration={`PT${duration}S`}
mediaPresentationDuration={`PT${await getDuration()}S`}
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
>
Expand Down Expand Up @@ -227,19 +229,21 @@ function DashManifest({

export function toDash(
streaming_data?: IStreamingData,
is_post_live_dvr = false,
url_transformer: URLTransformer = (url) => url,
format_filter?: FormatFilter,
cpn?: string,
player?: Player,
actions?: Actions,
storyboards?: PlayerStoryboardSpec
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec
) {
if (!streaming_data)
throw new InnertubeError('Streaming data not available');

return DashUtils.renderToString(
<DashManifest
streamingData={streaming_data}
isPostLiveDvr={is_post_live_dvr}
transformURL={url_transformer}
rejectFormat={format_filter}
cpn={cpn}
Expand Down
Loading

0 comments on commit 6dd03e1

Please sign in to comment.