@eplayer/emp4-core
v0.0.3
Published
EMP4 视频转换核心库 - 提供加密、编码、FFmpeg封装等核心功能
Maintainers
Readme
@eplayer/emp4-core
EMP4 视频转换核心库 — 提供加密、编码、FFmpeg 封装等核心功能。
目录
简介
@eplayer/emp4-core 是 EMP4 转换器的底层核心库,负责视频文件的加密编码、格式转换和流媒体清单生成。EMP4 是一种基于 AES-256-GCM 加密的视频容器格式,支持分片播放和多码率自适应流。
该库提供:
- 底层 API — 直接操作加密、编码、转码等核心功能
- 类型安全 — 完整的 TypeScript 类型定义
- 模块化设计 — 各模块职责明确,可独立使用
- 流式处理 — 支持大文件分块加密,内存占用可控
功能特性
| 特性 | 说明 | |------|------| | AES-256-GCM 加密 | 使用 PBKDF2 密钥派生 + GCM 认证加密,提供机密性和完整性保护 | | FFmpeg 封装 | 统一的转码、探针、缩略图提取接口,支持进度回调 | | 单文件模式 | 将整个视频编码为一个 EMP4 文件,适合离线播放 | | 分片模式 | 按时长分割为多个 EMP4 分片,适合流媒体传输 | | EPL 清单生成 | 类似 HLS m3u8 的加密流媒体清单(JSON 格式) | | 多码率自适应 | 支持同时生成多个分辨率/码率版本 | | PIN 保护 | 可选的额外 PIN 码访问控制 | | 完整 TypeScript 类型 | 全量类型定义导出,IDE 友好 |
EMP4 文件格式
整体结构
┌──────────────────────────────────────┐
│ 文件头部 (128 字节) │ 魔数 + 版本 + 盐值 + IV + 校验和 + 元数据长度
├──────────────────────────────────────┤
│ 元数据 (JSON) │ 视频信息:分辨率、码率、编解码器等
├──────────────────────────────────────┤
│ 加密数据块 1 │ 4字节长度 + 密文 + 16字节 GCM 标签
├──────────────────────────────────────┤
│ 加密数据块 2 │
│ ... │
├──────────────────────────────────────┤
│ 文件尾部 (12 字节) │ 4字节魔数 + 8字节总大小
└──────────────────────────────────────┘文件头部结构(128 字节)
| 偏移量 | 大小 | 描述 | |--------|------|------| | 0 | 4 | 魔数 "EMP4" | | 4 | 8 | 版本号(如 "1.0.0") | | 12 | 4 | 头部大小(128) | | 16 | 1 | 加密算法标识(0x00 = AES-256-GCM) | | 17 | 1 | 密钥派生算法(0x00 = PBKDF2-SHA256) | | 18 | 32 | 盐值(Salt) | | 50 | 12 | 初始化向量(IV) | | 62 | 4 | 原始格式(如 "mp4") | | 66 | 8 | 原始文件大小 | | 74 | 32 | 校验和(SHA-256) | | 106 | 4 | 元数据长度 | | 110 | 18 | 保留字段 |
加密数据块结构
每个加密数据块包含:
┌──────────────┬──────────────────┬──────────────────┐
│ 4 字节长度 │ 加密数据 │ 16 字节标签 │
│ (Little Endian)│ (密文) │ (GCM Auth Tag) │
└──────────────┴──────────────────┴──────────────────┘文件尾部结构(12 字节)
| 偏移量 | 大小 | 描述 | |--------|------|------| | 0 | 4 | 魔数 "EMP4" | | 4 | 8 | 文件总大小(Big Endian) |
安装
npm install @eplayer/emp4-core环境要求
- Node.js: >= 18.0.0
- FFmpeg: 需要系统安装 FFmpeg 并确保
ffmpeg和ffprobe命令可用
验证 FFmpeg 安装
ffmpeg -version
ffprobe -version快速开始
基本用法
import { EMP4Encoder, FFmpegWrapper, mergeConfig } from '@eplayer/emp4-core'
// 1. 配置
const config = mergeConfig({
ffmpegPath: 'ffmpeg',
ffprobePath: 'ffprobe',
defaultPassword: 'your-secret-password'
})
// 2. 获取视频信息
const ffmpeg = new FFmpegWrapper(config.ffmpegPath, config.ffprobePath)
const videoInfo = await ffmpeg.getVideoInfo('input.mp4')
// 3. 创建编码器并编码
const encoder = new EMP4Encoder(config.defaultPassword)
await encoder.encode('input.mp4', 'output.emp4', videoInfo)使用 Converter 命名空间
import Converter from '@eplayer/emp4-core'
// 通过统一入口访问所有功能
const config = Converter.mergeConfig({ ffmpegPath: 'ffmpeg' })
const ffmpeg = new Converter.FFmpegWrapper(config.ffmpegPath, config.ffprobePath)
const encoder = new Converter.EMP4Encoder(config.defaultPassword)分片模式(流媒体)
import { EMP4Encoder, FFmpegWrapper, EPLGenerator, generateKeyId } from '@eplayer/emp4-core'
const ffmpeg = new FFmpegWrapper()
const encoder = new EMP4Encoder('your-password')
const eplGenerator = new EPLGenerator(ffmpeg)
// 1. 获取视频信息
const videoInfo = await ffmpeg.getVideoInfo('input.mp4')
// 2. 将视频分割为 MP4 分片
const segments = await ffmpeg.segmentMp4('input.mp4', './output/segments', {
videoCodec: 'libx264',
audioCodec: 'aac',
segmentDuration: 10,
fragmented: true
})
// 3. 加密每个分片
const segmentResults = await encoder.encodeSegments(
segments,
'./output/encrypted',
videoInfo,
{ password: 'your-password' }
)
// 4. 生成 EPL 清单
const manifest = await eplGenerator.generate(videoInfo, [
{ filePath: './output/encrypted/segment-0.emp4', resolution: '1080p', width: 1920, height: 1080, bitrate: 8000000 }
], {
keyId: generateKeyId(),
outputDir: './output'
})
await eplGenerator.writeManifest(manifest, './output/manifest.epl')带进度回调
await encoder.encode('input.mp4', 'output.emp4', videoInfo, {
onProgress: (info) => {
console.log(`进度: ${info.percentage}%`)
console.log(`阶段: ${info.stage}`)
}
})模块详解
配置管理 (config)
配置管理模块提供默认配置、配置合并和验证功能。
默认配置
import { DEFAULT_CONFIG } from '@eplayer/emp4-core'
console.log(DEFAULT_CONFIG)
// {
// ffmpegPath: 'ffmpeg',
// ffprobePath: 'ffprobe',
// defaultPassword: 'default-encryption-key',
// defaultChunkSize: 1048576, // 1MB
// defaultSegmentDuration: 10, // 10秒
// defaultPreset: 'medium',
// defaultConcurrency: 4
// }合并配置
import { mergeConfig } from '@eplayer/emp4-core'
const config = mergeConfig({
ffmpegPath: '/usr/local/bin/ffmpeg',
defaultPassword: 'my-secret-key',
defaultChunkSize: 2 * 1024 * 1024 // 2MB
})验证配置
import { validateConfig } from '@eplayer/emp4-core'
const result = validateConfig(config)
if (!result.valid) {
console.error('配置错误:', result.errors)
}配置选项说明
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| ffmpegPath | string | 'ffmpeg' | FFmpeg 可执行文件路径 |
| ffprobePath | string | 'ffprobe' | FFprobe 可执行文件路径 |
| defaultPassword | string | 'default-encryption-key' | 默认加密密码 |
| defaultChunkSize | number | 1048576 | 默认分块大小(字节),最小 512KB |
| defaultSegmentDuration | number | 10 | 默认分片时长(秒) |
| defaultPreset | string | 'medium' | 默认编码预设 |
| defaultConcurrency | number | 4 | 默认并发数 |
加密引擎 (crypto)
加密引擎模块提供 AES-256-GCM 加密、解密和文件头尾构建功能。
创建加密引擎
import { CryptoEngine } from '@eplayer/emp4-core'
const crypto = new CryptoEngine('my-password')密钥派生
// 生成密钥参数(盐值和 IV)
const keyParams = crypto.generateKeyParams()
console.log('盐值长度:', keyParams.salt.length) // 32 字节
console.log('IV 长度:', keyParams.iv.length) // 12 字节
// 从密码派生密钥
const key = crypto.deriveKey('my-password', keyParams.salt)
console.log('密钥长度:', key.length) // 32 字节 (256 位)加密与解密
const data = Buffer.from('Hello, World!')
// 加密
const encrypted = crypto.encryptChunk(data, key, keyParams.iv)
console.log('密文长度:', encrypted.data.length)
console.log('认证标签长度:', encrypted.authTag.length) // 16 字节
// 解密
const decrypted = crypto.decryptChunk(
encrypted.data,
encrypted.authTag,
key,
keyParams.iv
)
console.log('解密结果:', decrypted.toString()) // "Hello, World!"分块 IV 生成
每个数据块使用不同的 IV 以提高安全性:
const chunk0IV = crypto.generateChunkIV(baseIV, 0)
const chunk1IV = crypto.generateChunkIV(baseIV, 1)文件头尾构建
import { buildHeader, buildFooter, parseHeader, parseFooter, HEADER_SIZE, FOOTER_SIZE } from '@eplayer/emp4-core'
// 构建头部
const header = buildHeader({
salt: keyParams.salt,
iv: keyParams.iv,
format: 'mp4',
originalSize: BigInt(1024000),
checksum: crypto.calculateChecksum(Buffer.from('data')),
metadataLength: 256
})
// 构建尾部
const footer = buildFooter(BigInt(1024000 + HEADER_SIZE + 256 + FOOTER_SIZE))
// 解析头部
const headerInfo = parseHeader(header)
console.log('魔数:', headerInfo.magic) // "EMP4"
console.log('版本:', headerInfo.version) // "1.0.0"
console.log('盐值:', headerInfo.salt)
console.log('IV:', headerInfo.iv)
// 解析尾部
const footerInfo = parseFooter(footer)
console.log('总大小:', footerInfo.totalSize)常量导出
import { HEADER_SIZE, FOOTER_SIZE, MAGIC, VERSION } from '@eplayer/emp4-core'
console.log('头部大小:', HEADER_SIZE) // 128
console.log('尾部大小:', FOOTER_SIZE) // 12
console.log('魔数:', MAGIC) // "EMP4"
console.log('版本:', VERSION) // "1.0.0"PIN 码保护功能
PIN 码保护为视频文件提供额外的访问控制层。用户必须输入正确的 PIN 码才能播放视频。
职责分离说明
PIN 码功能分为两个端:
| 端 | 库 | 职责 | 方法 |
|---|---|---|---|
| 加密端 | @eplayer/emp4-core (Node.js) | 生成 PIN 保护信息 | generatePinSalt(), hashPin(), generatePinProtection() |
| 解密端 | @eplayer/epl.js (浏览器) | 验证 PIN 并解密 | verifyPin(), deriveKeyFromPin() |
converter/core (加密端) epl (解密端)
┌─────────────────────┐ ┌─────────────────────┐
│ CryptoEngine │ │ PinVerifier │
│ ─────────────────── │ │ ─────────────────── │
│ generatePinSalt() │ │ verifyPin() │
│ hashPin() │────────────│ hashPin() │
│ generatePinProtection() │ deriveKeyFromPin() │
└─────────────────────┘ └─────────────────────┘
│ │
│ 写入元数据 │ 读取元数据
▼ ▼
EMP4 文件 播放器验证 PIN
{ hasPin: true, 并派生解密密钥
pinSalt: "...",
pinHash: "..." }生成 PIN 保护信息
import { CryptoEngine } from '@eplayer/emp4-core'
const crypto = new CryptoEngine()
// 为 PIN 码生成保护信息
const pinProtection = crypto.generatePinProtection('1234')
console.log('PIN 盐值 (Base64):', pinProtection.pinSalt)
console.log('PIN 哈希 (Base64):', pinProtection.pinHash)
// 返回值结构
// {
// salt: Buffer, // 16 字节的 PIN 盐值
// hash: Buffer, // 32 字节的 PIN 哈希值
// pinSalt: string, // Base64 编码的盐值,用于存储到元数据
// pinHash: string // Base64 编码的哈希值,用于存储到元数据
// }单独生成 PIN 盐值和哈希
// 生成 PIN 盐值(16 字节)
const pinSalt = crypto.generatePinSalt()
// 计算 PIN 哈希值
const pinHash = crypto.hashPin('1234', pinSalt)
// 转换为 Base64 存储
const pinSaltBase64 = pinSalt.toString('base64')
const pinHashBase64 = pinHash.toString('base64')PIN 码算法参数
| 参数 | 值 | 说明 | |------|-----|------| | 算法 | PBKDF2-HMAC-SHA256 | 与浏览器端 EPL 库保持一致 | | 迭代次数 | 100,000 | 增加暴力破解难度 | | 密钥长度 | 32 字节 (256 位) | PBKDF2 输出长度 | | PIN 盐值长度 | 16 字节 | 区别于加密盐值(32 字节) |
完整示例:带 PIN 保护的编码
import { CryptoEngine, EMP4Encoder, FFmpegWrapper } from '@eplayer/emp4-core'
const ffmpeg = new FFmpegWrapper()
const encoder = new EMP4Encoder('your-password')
const crypto = new CryptoEngine()
// 获取视频信息
const videoInfo = await ffmpeg.getVideoInfo('input.mp4')
// 生成 PIN 保护信息
const pinProtection = crypto.generatePinProtection('1234')
// 编码时启用 PIN 保护
await encoder.encode('input.mp4', 'output.emp4', videoInfo, {
title: '受保护的视频',
password: 'your-password',
hasPin: true,
pinSalt: pinProtection.pinSalt,
pinHash: pinProtection.pinHash
})FFmpeg 封装 (ffmpeg)
FFmpeg 封装模块提供视频信息获取、转码、分片和缩略图提取功能。
创建 FFmpeg 包装器
import { FFmpegWrapper } from '@eplayer/emp4-core'
const ffmpeg = new FFmpegWrapper('ffmpeg', 'ffprobe')检查工具可用性
const available = await ffmpeg.checkAvailable()
if (!available) {
throw new Error('FFmpeg 未安装或不可用')
}获取视频信息
const videoInfo = await ffmpeg.getVideoInfo('video.mp4')
console.log('时长:', videoInfo.duration, '秒')
console.log('大小:', videoInfo.size, '字节')
console.log('码率:', videoInfo.bitrate, 'bps')
console.log('容器:', videoInfo.container)
// 视频流信息
const videoStream = videoInfo.videoStreams[0]
console.log('视频编解码器:', videoStream.codec)
console.log('分辨率:', videoStream.width, 'x', videoStream.height)
console.log('帧率:', videoStream.fps, 'fps')
// 音频流信息
const audioStream = videoInfo.audioStreams[0]
console.log('音频编解码器:', audioStream.codec)
console.log('采样率:', audioStream.sampleRate, 'Hz')
console.log('声道数:', audioStream.channels)判断是否需要转码
const decision = ffmpeg.needsTranscode(videoInfo)
if (decision.required) {
console.log('需要转码,原因:', decision.reasons)
console.log('推荐配置:', decision.targetConfig)
}兼容性要求:
- 视频编解码器:H.264、H.265/HEVC、AVC
- 音频编解码器:AAC、MP3
- 容器格式:MP4、MOV、QuickTime
视频转码
await ffmpeg.transcode('input.mp4', 'output.mp4', {
videoCodec: 'libx264',
audioCodec: 'aac',
preset: 'medium',
crf: 23,
audioBitrate: '128k',
resolution: '1920x1080',
fps: 30,
fragmented: true
}, (progress) => {
console.log(`转码进度: ${progress}%`)
})转码配置选项
| 选项 | 类型 | 说明 |
|------|------|------|
| videoCodec | 'libx264' \| 'libx265' \| 'copy' | 视频编解码器 |
| audioCodec | 'aac' \| 'mp3' \| 'copy' | 音频编解码器 |
| preset | string | 编码预设:ultrafast ~ veryslow |
| crf | number | 恒定质量因子(0-51,默认 23) |
| audioBitrate | string | 音频码率(如 "128k") |
| resolution | string | 输出分辨率(如 "1920x1080") |
| fps | number | 输出帧率 |
| fragmented | boolean | 是否生成分片 MP4 |
| chunkSize | number | 分块大小(字节) |
| segmentDuration | number | 分片时长(秒) |
视频分片
const segments = await ffmpeg.segmentMp4('input.mp4', './segments', {
videoCodec: 'libx264',
audioCodec: 'aac',
segmentDuration: 10,
fragmented: true
}, (progress) => {
console.log(`分片进度: ${progress}%`)
})
// segments 是 SegmentSource 数组
for (const seg of segments) {
console.log(`分片 ${seg.index}: ${seg.filePath}, ${seg.duration}秒, ${seg.size}字节`)
}提取缩略图
// 提取到文件
await ffmpeg.getThumbnail('video.mp4', 10, 'thumbnail.jpg')
// 提取为 Buffer
const thumbBuffer = await ffmpeg.getThumbnail('video.mp4', 10)默认码率配置
import { DEFAULT_BITRATE_PROFILES } from '@eplayer/emp4-core'
// [
// { name: '1080p', width: 1920, height: 1080, videoBitrate: 8000000, audioBitrate: 192000 },
// { name: '720p', width: 1280, height: 720, videoBitrate: 5000000, audioBitrate: 128000 },
// { name: '480p', width: 854, height: 480, videoBitrate: 2500000, audioBitrate: 128000 }
// ]EMP4 编码器 (encoder)
EMP4 编码器模块提供视频文件到 EMP4 格式的编码功能。
创建编码器
import { EMP4Encoder } from '@eplayer/emp4-core'
const encoder = new EMP4Encoder('my-password', 1024 * 1024) // 密码, 分块大小单文件编码
await encoder.encode('input.mp4', 'output.emp4', videoInfo, {
title: '我的视频',
password: 'optional-override-password',
chunkSize: 2 * 1024 * 1024, // 2MB
hasPin: false,
onProgress: (info) => {
console.log(`${info.percentage}% - ${info.stage}`)
}
})编码选项
| 选项 | 类型 | 说明 |
|------|------|------|
| title | string | 视频标题 |
| password | string | 覆盖默认密码 |
| hasPin | boolean | 是否启用 PIN 保护 |
| pinSalt | string | PIN 盐值 |
| pinHash | string | PIN 哈希值 |
| chunkSize | number | 分块大小(字节) |
| onProgress | (info: ProgressInfo) => void | 进度回调 |
分片编码
const segmentResults = await encoder.encodeSegments(
segments, // SegmentSource[]
'./output', // 输出目录
videoInfo, // DetailedVideoInfo
{ password: 'my-password' }
)
// segmentResults 是 SegmentResult[]
for (const result of segmentResults) {
console.log(`分片 ${result.index}: ${result.filePath}, ${result.size}字节`)
console.log(`校验和: ${result.checksum}`)
}读取元数据
const metadata = await encoder.readMetadata('video.emp4')
console.log('标题:', metadata.title)
console.log('时长:', metadata.duration)
console.log('分辨率:', metadata.resolution)
console.log('创建时间:', metadata.createdAt)工具函数
import { isEMP4File, getDefaultOutputPath, getVideoOutputDir } from '@eplayer/emp4-core'
// 检查文件是否为 EMP4 格式
const isEMP4 = await isEMP4File('video.emp4')
// 获取默认输出路径
const outputPath = getDefaultOutputPath('video.mp4')
// 返回: 'video/manifest.epl'
// 获取视频输出目录
const outputDir = getVideoOutputDir('video.mp4', './output')
// 返回: './output/video'EPL 清单生成器 (manifest)
EPL 清单生成器模块负责生成 EPL(Encrypted Playlist)流媒体清单文件。
创建生成器
import { EPLGenerator, FFmpegWrapper } from '@eplayer/emp4-core'
const ffmpeg = new FFmpegWrapper()
const eplGenerator = new EPLGenerator(ffmpeg)生成清单
import { generateKeyId } from '@eplayer/emp4-core'
const manifest = await eplGenerator.generate(videoInfo, outputs, {
title: '我的视频',
keyId: generateKeyId(),
keyServer: 'https://key-server.example.com',
thumbnailInterval: 30,
outputDir: './output',
chunkSize: 1024 * 1024,
segmentDuration: 10
})EPL 清单结构
interface EPLManifest {
version: string // "1.0"
type: 'vod' | 'live' // 流类型
duration: number // 总时长(秒)
title: string // 标题
encryption: {
method: 'AES-256-GCM'
keyId: string
keyServer?: string
}
chunkConfig: {
defaultSize: number
defaultDuration: number
minSize?: number
maxSize?: number
}
streams: StreamInfo[] // 多码率流列表
thumbnails: ThumbnailInfo[] // 缩略图列表
chapters?: ChapterInfo[] // 章节列表(可选)
}写入清单文件
await eplGenerator.writeManifest(manifest, './output/manifest.epl')生成缩略图
const thumbnails = await eplGenerator.generateThumbnails(
'video.mp4',
'./output/thumbnails',
30, // 间隔(秒)
3600 // 总时长(秒)
)
// thumbnails: ThumbnailInfo[]
for (const thumb of thumbnails) {
console.log(`时间: ${thumb.time}s, URI: ${thumb.uri}`)
}生成密钥 ID
import { generateKeyId } from '@eplayer/emp4-core'
const keyId = generateKeyId()
// 返回 32 字符的十六进制字符串,如 "a1b2c3d4e5f6..."类型定义
视频信息类型
// 基础视频信息
interface VideoInfo {
path: string
name: string
format: string
duration: number
resolution: string
bitrate: number
codec: string
audioCodec: string
fileSize: number
width?: number
height?: number
fps?: number
}
// 详细视频信息
interface DetailedVideoInfo {
duration: number
size: number
bitrate: number
container: string
videoStreams: VideoStream[]
audioStreams: AudioStream[]
}
// 视频流信息
interface VideoStream {
codec: string
width: number
height: number
fps: number
profile?: string
level?: number
bitrate?: number
}
// 音频流信息
interface AudioStream {
codec: string
sampleRate: number
channels: number
bitrate?: number
}转码配置类型
interface TranscodeConfig {
videoCodec?: 'libx264' | 'libx265' | 'copy'
audioCodec?: 'aac' | 'mp3' | 'copy'
preset?: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'
crf?: number
audioBitrate?: string
resolution?: string
fps?: number
fragmented?: boolean
chunkSize?: number
segmentDuration?: number
}
interface TranscodeDecision {
required: boolean
reasons: string[]
targetConfig: TranscodeConfig | null
}加密相关类型
interface KeyParams {
salt: Buffer
iv: Buffer
}
interface EncryptedChunk {
data: Buffer
authTag: Buffer
}
interface HeaderParams {
salt: Buffer
iv: Buffer
format: string
originalSize: bigint
checksum: Buffer
metadataLength: number
}EMP4 相关类型
interface EMP4Metadata {
title?: string
duration: number
resolution: string
bitrate: number
codec: string
audioCodec: string
createdAt: string
fileSize: number
fragmented: boolean
hasPin?: boolean
pinSalt?: string
pinHash?: string
}
interface EMP4Output {
filePath: string
resolution: string
width: number
height: number
bitrate: number
}
interface SegmentResult {
index: number
filePath: string
size: number
checksum: string
}
interface SegmentSource {
index: number
filePath: string
duration: number
size: number
}EPL 清单类型
interface EPLManifest {
version: string
type: 'vod' | 'live'
duration: number
title: string
encryption: EncryptionInfo
chunkConfig: ChunkConfig
streams: StreamInfo[]
thumbnails: ThumbnailInfo[]
chapters?: ChapterInfo[]
}
interface StreamInfo {
id: string
name: string
bandwidth: number
resolution: string
codecs: string
frameRate: number
segments: SegmentInfo[]
}
interface SegmentInfo {
index: number
duration: number
size: number
uri: string
checksum: string
}
interface ThumbnailInfo {
time: string
uri: string
}
interface ChapterInfo {
title: string
startTime: number
endTime: number
}进度信息类型
interface ProgressInfo {
percentage: number
stage: 'analyzing' | 'transcoding' | 'encrypting' | 'generating' | 'completed'
currentProfile?: string
eta?: number
speed?: string
}完整示例
完整转换流程
import {
FFmpegWrapper,
EMP4Encoder,
EPLGenerator,
mergeConfig,
generateKeyId,
DEFAULT_BITRATE_PROFILES
} from '@eplayer/emp4-core'
async function convertToEMP4(inputPath: string, outputDir: string) {
// 1. 配置
const config = mergeConfig({
ffmpegPath: 'ffmpeg',
defaultPassword: 'my-secret-password',
defaultSegmentDuration: 10
})
// 2. 初始化组件
const ffmpeg = new FFmpegWrapper(config.ffmpegPath, config.ffprobePath)
const encoder = new EMP4Encoder(config.defaultPassword, config.defaultChunkSize)
const eplGenerator = new EPLGenerator(ffmpeg)
// 3. 获取视频信息
const videoInfo = await ffmpeg.getVideoInfo(inputPath)
console.log(`视频时长: ${videoInfo.duration}秒`)
// 4. 判断是否需要转码
const transcodeDecision = ffmpeg.needsTranscode(videoInfo)
let segments
if (transcodeDecision.required) {
console.log('需要转码:', transcodeDecision.reasons)
// 转码并分片
segments = await ffmpeg.segmentMp4(inputPath, `${outputDir}/temp`, {
...transcodeDecision.targetConfig,
segmentDuration: config.defaultSegmentDuration
}, (progress) => {
console.log(`转码进度: ${progress}%`)
})
} else {
// 直接分片
segments = await ffmpeg.segmentMp4(inputPath, `${outputDir}/temp`, {
videoCodec: 'copy',
audioCodec: 'copy',
segmentDuration: config.defaultSegmentDuration,
fragmented: true
})
}
// 5. 加密分片
const encryptedSegments = await encoder.encodeSegments(
segments,
`${outputDir}/streams/1080p`,
videoInfo,
{
onProgress: (info) => {
console.log(`加密进度: ${info.percentage}%`)
}
}
)
// 6. 生成缩略图
const thumbnails = await eplGenerator.generateThumbnails(
inputPath,
`${outputDir}/thumbnails`,
30,
videoInfo.duration
)
// 7. 生成 EPL 清单
const manifest = await eplGenerator.generate(videoInfo, [{
filePath: encryptedSegments[0].filePath,
resolution: '1080p',
width: videoInfo.videoStreams[0].width,
height: videoInfo.videoStreams[0].height,
bitrate: videoInfo.bitrate
}], {
title: 'My Video',
keyId: generateKeyId(),
outputDir,
thumbnailInterval: 30,
segmentDuration: config.defaultSegmentDuration
})
manifest.thumbnails = thumbnails
// 8. 写入清单
await eplGenerator.writeManifest(manifest, `${outputDir}/manifest.epl`)
console.log('转换完成!')
return manifest
}
// 使用
convertToEMP4('./input.mp4', './output')多码率转换
import { FFmpegWrapper, EMP4Encoder, EPLGenerator, generateKeyId, DEFAULT_BITRATE_PROFILES } from '@eplayer/emp4-core'
async function convertMultiBitrate(inputPath: string, outputDir: string) {
const ffmpeg = new FFmpegWrapper()
const encoder = new EMP4Encoder('password')
const eplGenerator = new EPLGenerator(ffmpeg)
const videoInfo = await ffmpeg.getVideoInfo(inputPath)
const outputs = []
for (const profile of DEFAULT_BITRATE_PROFILES) {
const segmentDir = `${outputDir}/streams/${profile.name}`
// 分片转码
const segments = await ffmpeg.segmentMp4(inputPath, `${segmentDir}/temp`, {
videoCodec: 'libx264',
audioCodec: 'aac',
resolution: `${profile.width}x${profile.height}`,
segmentDuration: 10,
fragmented: true
})
// 加密
const results = await encoder.encodeSegments(segments, segmentDir, videoInfo)
outputs.push({
filePath: results[0].filePath,
resolution: profile.name,
width: profile.width,
height: profile.height,
bitrate: profile.videoBitrate + profile.audioBitrate
})
}
// 生成清单
const manifest = await eplGenerator.generate(videoInfo, outputs, {
keyId: generateKeyId(),
outputDir
})
await eplGenerator.writeManifest(manifest, `${outputDir}/manifest.epl`)
}构建与开发
安装依赖
npm install构建
# 完整构建(类型声明 + JS 打包)
npm run build
# 仅生成类型声明
npm run build:types
# 仅打包 JS
npm run build:js清理
npm run clean构建产物
dist/
├── index.js # ESM 格式打包产物
├── index.js.map # Source Map
├── index.d.ts # 类型声明入口
└── *.d.ts # 各模块类型声明