-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathVideoTranscoder.ts
More file actions
133 lines (111 loc) · 4.35 KB
/
VideoTranscoder.ts
File metadata and controls
133 lines (111 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { spawn } from 'child_process';
import { createReadStream, promises as fsPromises } from 'fs';
import * as path from 'path';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
export class VideoTranscoder {
private s3Client: S3Client;
private bucketName: string;
constructor(region: string, bucketName: string) {
this.s3Client = new S3Client({ region });
this.bucketName = bucketName;
}
/**
* Process a video to adaptive HLS (1080p, 720p, 480p) and upload to S3.
* @param inputFilePath Absolute path to the raw uploaded video
* @param outputDir Temporary local directory to store HLS segments
* @param s3DestinationPrefix S3 folder path (e.g., 'videos/user123/stream456')
*/
public async processAndUpload(
inputFilePath: string,
outputDir: string,
s3DestinationPrefix: string
): Promise<void> {
try {
// 1. Ensure output directory exists
await fsPromises.mkdir(outputDir, { recursive: true });
// 2. Transcode to HLS using FFmpeg
await this.transcodeToHLS(inputFilePath, outputDir);
// 3. Upload outputs to S3
await this.uploadDirectoryToS3(outputDir, s3DestinationPrefix);
} finally {
// 4. Cleanup local files
await fsPromises.rm(outputDir, { recursive: true, force: true }).catch(() => {
console.warn(`Failed to clean up temp directory: ${outputDir}`);
});
}
}
/**
* Generates 1080p, 720p, and 480p HLS playlists using FFmpeg.
*/
private transcodeToHLS(inputFilePath: string, outputDir: string): Promise<void> {
return new Promise((resolve, reject) => {
const args = [
'-y', // Overwrite existing files
'-i', inputFilePath,
'-preset', 'veryfast', // Optimization for processing speed
'-g', '48', // Keyframe interval (assuming ~24fps, keyframe every 2 secs)
'-sc_threshold', '0',
// Map video and audio streams 3 times for our 3 variants
'-map', '0:v:0', '-map', '0:a:0',
'-map', '0:v:0', '-map', '0:a:0',
'-map', '0:v:0', '-map', '0:a:0',
// Variant 0: 1080p
'-s:v:0', '1920x1080', '-c:v:0', 'libx264', '-b:v:0', '5000k',
// Variant 1: 720p
'-s:v:1', '1280x720', '-c:v:1', 'libx264', '-b:v:1', '2800k',
// Variant 2: 480p
'-s:v:2', '854x480', '-c:v:2', 'libx264', '-b:v:2', '1400k',
// Audio settings (standardized for all variants)
'-c:a', 'aac', '-b:a', '128k',
// Define the stream mapping for the master playlist
'-var_stream_map', 'v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:480p',
// HLS output configuration
'-master_pl_name', 'master.m3u8',
'-f', 'hls',
'-hls_time', '4', // 4 second segment duration
'-hls_playlist_type', 'vod',
'-hls_segment_filename', path.join(outputDir, '%v_sequence_%d.ts'),
path.join(outputDir, '%v_playlist.m3u8')
];
const ffmpeg = spawn('ffmpeg', args);
ffmpeg.stderr.on('data', (data) => {
// FFmpeg writes progress to stderr. Can be logged in trace mode if needed.
});
ffmpeg.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`FFmpeg process exited with code ${code}`));
}
});
ffmpeg.on('error', (err) => {
reject(new Error(`Failed to start FFmpeg: ${err.message}`));
});
});
}
/**
* Uploads the generated `.m3u8` and `.ts` files to the configured S3 Bucket.
*/
private async uploadDirectoryToS3(dirPath: string, prefix: string): Promise<void> {
const files = await fsPromises.readdir(dirPath);
const uploadPromises = files.map(async (file) => {
const filePath = path.join(dirPath, file);
const s3Key = path.posix.join(prefix, file);
let contentType = 'application/octet-stream';
if (file.endsWith('.m3u8')) {
contentType = 'application/x-mpegURL';
} else if (file.endsWith('.ts')) {
contentType = 'video/MP2T';
}
const command = new PutObjectCommand({
Bucket: this.bucketName,
Key: s3Key,
Body: createReadStream(filePath),
ContentType: contentType,
});
await this.s3Client.send(command);
});
// Upload all segments and playlists in parallel
await Promise.all(uploadPromises);
}
}