136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
import { join } from 'node:path';
|
|
import { execMP4Box } from './utils';
|
|
import type { VideoProfile } from './types';
|
|
|
|
/**
|
|
* Package MP4 files into DASH format using MP4Box
|
|
* Stage 2: Light work - just packaging, no encoding
|
|
* Creates one master MPD manifest with all profiles
|
|
*/
|
|
export async function packageToDash(
|
|
mp4Files: Map<string, string>,
|
|
outputDir: string,
|
|
profiles: VideoProfile[],
|
|
segmentDuration: number
|
|
): Promise<string> {
|
|
const manifestPath = join(outputDir, 'manifest.mpd');
|
|
|
|
// Build MP4Box command
|
|
const args = [
|
|
'-dash', String(segmentDuration * 1000), // MP4Box expects milliseconds
|
|
'-frag', String(segmentDuration * 1000),
|
|
'-rap', // Force segments to start with random access points
|
|
'-segment-name', '$RepresentationID$_$Number$',
|
|
'-out', manifestPath
|
|
];
|
|
|
|
// Add all MP4 files with their profile IDs
|
|
for (const profile of profiles) {
|
|
const mp4Path = mp4Files.get(profile.name);
|
|
if (!mp4Path) {
|
|
throw new Error(`MP4 file not found for profile: ${profile.name}`);
|
|
}
|
|
|
|
// Add video track with representation ID
|
|
args.push(`${mp4Path}#video:id=${profile.name}`);
|
|
// Add audio track (shared across all profiles)
|
|
if (profile === profiles[0]) {
|
|
args.push(`${mp4Path}#audio:id=audio`);
|
|
}
|
|
}
|
|
|
|
// Execute MP4Box
|
|
await execMP4Box(args);
|
|
|
|
// MP4Box creates files in the same directory as output MPD
|
|
// Move segment files to profile subdirectories for clean structure
|
|
await organizeSegments(outputDir, profiles);
|
|
|
|
// Update MPD to reflect new file structure with subdirectories
|
|
await updateManifestPaths(manifestPath, profiles);
|
|
|
|
return manifestPath;
|
|
}
|
|
|
|
/**
|
|
* Organize segments into profile subdirectories
|
|
* MP4Box creates all files in one directory, we organize them
|
|
*/
|
|
async function organizeSegments(
|
|
outputDir: string,
|
|
profiles: VideoProfile[]
|
|
): Promise<void> {
|
|
const { readdir, rename, mkdir } = await import('node:fs/promises');
|
|
|
|
// Create profile subdirectories
|
|
for (const profile of profiles) {
|
|
const profileDir = join(outputDir, profile.name);
|
|
await mkdir(profileDir, { recursive: true });
|
|
}
|
|
|
|
// Create audio subdirectory
|
|
const audioDir = join(outputDir, 'audio');
|
|
await mkdir(audioDir, { recursive: true });
|
|
|
|
// Get all files in output directory
|
|
const files = await readdir(outputDir);
|
|
|
|
// Move segment files to their respective directories
|
|
for (const file of files) {
|
|
// Skip manifest
|
|
if (file === 'manifest.mpd') {
|
|
continue;
|
|
}
|
|
|
|
// Move audio files to audio/ directory
|
|
if (file.startsWith('audio_') || file === 'audio_init.m4s') {
|
|
const oldPath = join(outputDir, file);
|
|
const newPath = join(audioDir, file);
|
|
await rename(oldPath, newPath);
|
|
continue;
|
|
}
|
|
|
|
// Move video segment files to their profile directories
|
|
for (const profile of profiles) {
|
|
if (file.startsWith(`${profile.name}_`)) {
|
|
const oldPath = join(outputDir, file);
|
|
const newPath = join(outputDir, profile.name, file);
|
|
await rename(oldPath, newPath);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update MPD manifest to reflect subdirectory structure
|
|
*/
|
|
async function updateManifestPaths(
|
|
manifestPath: string,
|
|
profiles: VideoProfile[]
|
|
): Promise<void> {
|
|
const { readFile, writeFile } = await import('node:fs/promises');
|
|
|
|
let mpd = await readFile(manifestPath, 'utf-8');
|
|
|
|
// MP4Box uses $RepresentationID$ template variable
|
|
// Replace: media="$RepresentationID$_$Number$.m4s"
|
|
// With: media="$RepresentationID$/$RepresentationID$_$Number$.m4s"
|
|
|
|
mpd = mpd.replace(
|
|
/media="\$RepresentationID\$_\$Number\$\.m4s"/g,
|
|
'media="$RepresentationID$/$RepresentationID$_$Number$.m4s"'
|
|
);
|
|
|
|
// Replace: initialization="$RepresentationID$_.mp4"
|
|
// With: initialization="$RepresentationID$/$RepresentationID$_.mp4"
|
|
|
|
mpd = mpd.replace(
|
|
/initialization="\$RepresentationID\$_\.mp4"/g,
|
|
'initialization="$RepresentationID$/$RepresentationID$_.mp4"'
|
|
);
|
|
|
|
await writeFile(manifestPath, mpd, 'utf-8');
|
|
}
|
|
|