init
This commit is contained in:
135
src/packaging.ts
Normal file
135
src/packaging.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user