不依赖于FFmpeg没有协议法律风险,功能简单一些,做转码压缩依赖会小很多。
视频转码流水线双层循环结构详解
1. 第一层循环:Segment循环(音视频分段)
kotlin
// 在DefaultTranscodeEngine中
while (true) {
// 1. 获取音频和视频分段
val audio = segments.next(TrackType.AUDIO)
val video = segments.next(TrackType.VIDEO)
// 2. 推进分段处理
val advanced = (audio?.advance() ?: false) or (video?.advance() ?: false)
// 3. 检查是否完成
val completed = !advanced && !segments.hasNext()
if (completed) break
// 4. 处理进度更新
if (advanced) {
updateProgress()
}
}
这一层循环的职责:
-
分别处理音频和视频轨道
-
按分段推进处理
-
跟踪整体进度
-
控制转码过程的开始和结束
2. 第二层循环:Pipeline循环(处理步骤)
ini
// 在Pipeline.execute()中
for (i in items.indices) {
val item = items[i]
if (item.canHandle(i == 0)) {
val failure = item.handle()
// 处理结果...
}
}
这一层循环的职责:
-
顺序执行各个处理步骤(PipelineItem)
-
管理数据在步骤间的流转
-
处理步骤执行的结果
3. PipelineItem数据流转
rust
[输入] -> Reader -> Decoder -> VideoRenderer -> VideoPublisher -> Encoder -> Writer -> [输出]
每个PipelineItem:
-
通过unhandled队列接收输入数据
-
使用step.advance()处理单个数据
-
将结果传递给下一个PipelineItem的unhandled队列
4. 完整流程示例
css
[第一层:Segment循环]
├── 处理视频分段1
│ └── [第二层:Pipeline循环]
│ ├── Reader处理分段1数据
│ ├── Decoder解码分段1数据
│ ├── VideoRenderer渲染分段1
│ ├── VideoPublisher处理分段1
│ ├── Encoder编码分段1
│ └── Writer写入分段1
│
├── 处理音频分段1
│ └── [第二层:Pipeline循环]
│ ├── Reader处理音频数据
│ ├── Decoder解码音频
│ ├── AudioProcessor处理音频
│ ├── Encoder编码音频
│ └── Writer写入音频
│
├── 处理视频分段2
│ └── [第二层:Pipeline循环]
│ └── ...
│
└── 处理音频分段2
└── [第二层:Pipeline循环]
└── ...
5. 关键特性
5.1 数据传递机制
kotlin
fun attachToNext(next: PipelineItem) {
nextUnhandled = next.unhandled
step.initialize(next = next.step.channel)
}
-
通过unhandled队列在PipelineItem间传递数据
-
每个Step都有自己的Channel用于通信
5.2 状态控制
kotlin
return when {
items.isEmpty() -> State.Eos(Unit)
items.last().done -> State.Eos(Unit)
advanced -> State.Ok(Unit)
else -> State.Retry(sleeps)
}
-
精确跟踪处理状态
-
支持重试机制
-
正确处理结束条件
5.3 进度跟踪
scss
if (advanced && ++loop % PROGRESS_LOOPS == 0L) {
progress((videoProgress + audioProgress) / tracks.active.size)
}
-
分别跟踪音视频进度
-
定期更新总体进度
6. 优势
- 结构清晰
-
两层循环各司其职
-
职责划分明确
-
便于维护和调试
- 高效处理
-
分段处理减少内存占用
-
流水线并行提高效率
-
队列机制确保数据顺序
- 灵活扩展
-
易于添加新的处理步骤
-
支持不同媒体类型
-
可定制处理流程
这种双层循环结构既保证了转码过程的可靠性,又提供了足够的灵活性来处理各种媒体转码需求。
Pipeline中Step包装成PipelineItem的过程
1. Pipeline.build方法实现
kotlin
internal fun build(
name: String,
debug: String? = null,
builder: () -> Builder<*, Channel> = { Builder<Unit, Channel>() }
): Pipeline {
// 1. 获取构建器中的步骤列表
val steps = builder().steps
// 2. 将步骤列表转换为PipelineItem列表
val items = steps.mapIndexed { index, step ->
@Suppress("UNCHECKED_CAST")
PipelineItem(
// 将Step转换为通用类型
step = step as Step<Any, Channel, Any, Channel>,
// 创建带序号的名称,如"1/5 'Decoder'"
name = "${index+1}/${steps.size} '${step.name}'"
)
}
// 3. 创建Pipeline实例
return Pipeline("${name}Pipeline${debug ?: ""}", items)
}
2. Step组合机制
2.1 Builder类定义
kotlin
class Builder<D: Any, C: Channel> internal constructor(
internal val steps: List<Step<*, *, *, *>> = listOf()
) {
// 通过加号运算符添加新步骤
operator fun <NewData: Any, NewChannel: Channel> plus(
step: Step<D, C, NewData, NewChannel>
): Builder<NewData, NewChannel> = Builder(steps + step)
}
2.2 Step加号运算符重载
kotlin
internal operator fun <CurrData: Any, CurrChannel: Channel,
NewData: Any, NewChannel: Channel>
Step<Unit, Channel, CurrData, CurrChannel>.plus(
other: Step<CurrData, CurrChannel, NewData, NewChannel>
): Pipeline.Builder<NewData, NewChannel> {
// 创建包含两个步骤的Builder
return Pipeline.Builder<CurrData, CurrChannel>(listOf(this)) + other
}
3. 使用示例
scss
// 创建Pipeline的典型方式
val pipeline = Pipeline.build("VideoTranscode") {
Reader() + // 第一个Step
Decoder() + // 第二个Step
VideoRenderer() + // 第三个Step
VideoPublisher() + // 第四个Step
Encoder() + // 第五个Step
Writer() // 第六个Step
}
4. 数据流转类型
vbnet
// Step的泛型参数
Step<InputData, InputChannel, OutputData, OutputChannel>
// 例如:
Reader: Step<Unit, Channel, Frame, VideoChannel>
Decoder: Step<Frame, VideoChannel, DecodedFrame, VideoChannel>
VideoRenderer: Step<DecodedFrame, VideoChannel, RenderedFrame, VideoChannel>
// ...等
5. PipelineItem包装的作用
5.1 统一接口
kotlin
private class PipelineItem(
val step: Step<Any, Channel, Any, Channel>, // 统一类型
val name: String
) {
val unhandled = ArrayDeque<State.Ok<Any>>() // 统一数据队列
private var nextUnhandled: ArrayDeque<State.Ok<Any>>? = null
// ...
}
5.2 状态管理
kotlin
class PipelineItem {
//...
var done = false // 完成状态
var advanced = false // 处理状态
var packets = 0 // 数据包计数
}
5.3 数据传递
kotlin
fun attachToNext(next: PipelineItem) {
nextUnhandled = next.unhandled
step.initialize(next = next.step.channel)
}
6. 构建过程的特点
- 类型安全
-
Builder使用泛型确保Step之间的数据类型匹配
-
加号运算符重载保证了Step的正确连接
- 统一封装
-
所有Step被包装成统一的PipelineItem类型
-
简化了Pipeline的管理和执行逻辑
- 灵活组合
-
通过加号运算符实现Step的链式组合
-
支持动态构建处理流程
- 状态追踪
-
PipelineItem封装了处理状态
-
便于监控和调试
7. 构建流程总结
- Step定义
kotlin
class MyStep : Step<InputType, InputChannel, OutputType, OutputChannel>
- Step组合
scss
val builder = Step1() + Step2() + Step3()
- Pipeline构建
javascript
Pipeline.build("name") { builder }
- PipelineItem包装
arduino
steps.mapIndexed { index, step ->
PipelineItem(step, name)
}
- 项目连接
scss
items.zipWithNext().reversed().forEach {
(first, next) -> first.attachToNext(next)
}
这种设计实现了:
-
Step的灵活组合
-
类型安全的数据流转
-
统一的状态管理
-
简洁的API接口
Step与PipelineItem的数据流转机制
1. Step包装成PipelineItem
kotlin
private class PipelineItem(
val step: Step<Any, Channel, Any, Channel>, // 实际的处理步骤
val name: String, // 步骤名称
) {
// 未处理数据队列,用于存储等待处理的数据
val unhandled = ArrayDeque<State.Ok<Any>>()
// 指向下一个PipelineItem的未处理队列,用于数据传递
private var nextUnhandled: ArrayDeque<State.Ok<Any>>? = null
// 状态标记
var done = false // 是否处理完成
var advanced = false // 是否有数据被处理
var packets = 0 // 处理的数据包数量
}
2. PipelineItem间的数据衔接
2.1 连接机制
kotlin
fun attachToNext(next: PipelineItem) {
// 1. 建立队列连接:当前PipelineItem的输出连接到下一个的输入队列
nextUnhandled = next.unhandled
// 2. 初始化Step的Channel:确保Step级别的数据通道正确连接
step.initialize(next = next.step.channel)
}
2.2 Pipeline中的连接设置
scss
init {
// 从后向前连接所有PipelineItem
items.zipWithNext().reversed().forEach { (first, next) ->
first.attachToNext(next)
}
}
3. 数据流转过程
3.1 数据处理和传递
kotlin
fun handle(): State.Failure? {
advanced = false
while (unhandled.isNotEmpty() && !done) {
// 1. 从队列取出待处理数据
val input = unhandled.removeFirst()
// 2. 使用Step处理数据
when (val result = step.advance(input)) {
is State.Ok -> {
packets++
advanced = true
done = result is State.Eos
// 3. 将处理结果传递给下一个PipelineItem
nextUnhandled?.addLast(result)
}
is State.Retry -> {
// 处理失败,重新入队
unhandled.addFirst(input)
return result
}
is State.Consume -> return result
}
}
}
4. 实际数据流转示例
scss
[Reader PipelineItem]
↓ 读取视频帧
unhandled = [frame1]
step.advance(frame1)
nextUnhandled → [Decoder PipelineItem]
↓ 解码帧
unhandled = [decoded1]
step.advance(decoded1)
nextUnhandled → [VideoRenderer PipelineItem]
↓ 渲染帧
unhandled = [rendered1]
step.advance(rendered1)
nextUnhandled → [VideoPublisher PipelineItem]
↓ 发布帧
unhandled = [published1]
step.advance(published1)
nextUnhandled → [Encoder PipelineItem]
↓ 编码帧
unhandled = [encoded1]
step.advance(encoded1)
nextUnhandled → [Writer PipelineItem]
↓ 写入帧
step.advance(encoded1)
5. 特殊情况处理
5.1 QueuedStep处理
kotlin
if (!advanced && !done && step is QueuedStep) {
when (val result = step.tryAdvance()) {
is State.Ok -> {
packets++
advanced = true
done = result is State.Eos
nextUnhandled?.addLast(result)
}
is State.Failure -> return result
}
}
5.2 重试机制
kotlin
when (val result = step.advance(input)) {
is State.Retry -> {
// 将数据重新放回队列头部
unhandled.addFirst(input)
return result
}
}
6. 数据流转的优势
- 解耦性
-
Step只需关注数据处理逻辑
-
PipelineItem负责数据传递和状态管理
-
职责分离,易于维护
- 可靠性
-
队列机制确保数据顺序
-
支持重试处理
-
错误处理机制完善
- 灵活性
-
易于添加新的处理步骤
-
支持不同类型的数据处理
-
可动态调整处理流程
- 效率
-
使用双端队列优化数据操作
-
支持异步处理
-
内存使用可控
7. 工作流程总结
- 初始化阶段
-
Step被包装成PipelineItem
-
建立PipelineItem之间的连接
-
初始化Step的Channel
- 数据处理阶段
-
从unhandled队列获取数据
-
使用Step处理数据
-
将结果传递给下一个PipelineItem
- 状态管理
-
跟踪处理进度
-
处理特殊情况
-
确保数据正确流转
这种设计使得整个转码流程能够像流水线一样顺畅运行,每个Step专注于自己的处理逻辑,而PipelineItem则确保数据能够正确地在Steps之间传递和流转。
视频转码的第一层循环分片机制
1. 数据源级别的分片
实际上,数据源本身并不负责分片,它主要通过MediaExtractor提供了以下核心功能:
- 轨道分离
python
// 在initialize()中完成轨道分离
for (i = 0; i < trackCount; i++) {
MediaFormat format = mExtractor.getTrackFormat(i)
TrackType type = getTrackTypeOrNull(format)
if (type != null && !mIndex.has(type)) {
mIndex.set(type, i) // 记录轨道索引
mFormat.set(type, format) // 记录轨道格式
}
}
- 数据读取
arduino
// 在readTrack()中读取数据
int read = mExtractor.readSampleData(chunk.buffer, position)
chunk.timeUs = mExtractor.getSampleTime()
chunk.keyframe = (mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0
mExtractor.advance() // 移动到下一个样本
2. Segments层的分片管理
真正的分片管理是在Segments类中实现的:
kotlin
class Segments(
private val sources: DataSources, // 多个数据源
private val tracks: Tracks, // 轨道信息
private val factory: (TrackType, Int, Int, TrackStatus, MediaFormat) -> Pipeline
) {
// 当前处理的分段
private val current = mutableTrackMapOf<Segment>(null, null)
// 当前分段索引
val currentIndex = mutableTrackMapOf(-1, -1)
// 请求的分段索引
private val requestedIndex = mutableTrackMapOf(0, 0)
}
2.1 分片创建过程
kotlin
private fun tryCreateSegment(type: TrackType, index: Int): Segment? {
// 1. 获取数据源
val source = sources[type].getOrNull(index) ?: return null
// 2. 选择轨道
if (tracks.active.has(type)) {
source.selectTrack(type)
// 处理同源的其他轨道
val other = when (type) {
TrackType.AUDIO -> TrackType.VIDEO
TrackType.VIDEO -> TrackType.AUDIO
}
if (tracks.active.has(other) && sources[other].any { it === source }) {
source.selectTrack(other)
}
}
// 3. 创建Pipeline
val pipeline = factory(
type,
index,
sources[type].size,
tracks.all[type],
tracks.outputFormats[type]
)
// 4. 创建分段
return Segment(type, index, pipeline)
}
3. 分片处理流程
css
[DataSources]
│
├── 音频源列表 [DataSource1, DataSource2, ...]
│ └── MediaExtractor处理
│ ├── 读取音频样本
│ └── 时间戳管理
│
└── 视频源列表 [DataSource1, DataSource2, ...]
└── MediaExtractor处理
├── 读取视频样本
└── 时间戳管理
[Segments]
│
├── 音频分段管理
│ ├── 当前分段 (current[AUDIO])
│ ├── 当前索引 (currentIndex[AUDIO])
│ └── 请求索引 (requestedIndex[AUDIO])
│
└── 视频分段管理
├── 当前分段 (current[VIDEO])
├── 当前索引 (currentIndex[VIDEO])
└── 请求索引 (requestedIndex[VIDEO])
4. 分片特点
- 按轨道类型分离
-
音频和视频轨道分开处理
-
每个轨道可以有多个数据源
- 延迟加载
-
分段按需创建
-
资源使用更高效
- 同步处理
scss
// 处理同源的音视频轨道
if (tracks.active.has(other) && sources[other].any { it === source }) {
source.selectTrack(other)
}
- 状态管理
java
// 跟踪每个轨道的处理状态
private val current = mutableTrackMapOf<Segment>(null, null)
val currentIndex = mutableTrackMapOf(-1, -1)
private val requestedIndex = mutableTrackMapOf(0, 0)
5. 分片优势
- 内存效率
-
只加载当前处理的分段
-
及时释放已处理的资源
- 并行处理
-
音频和视频可以并行处理
-
提高处理效率
- 灵活控制
-
支持多数据源
-
可以动态调整处理顺序
- 精确定位
-
支持精确的时间戳控制
-
便于音视频同步
这种分片机制使得视频转码过程能够高效且可靠地处理大型媒体文件,同时保持较低的内存占用。通过Segments的管理,实现了对多个数据源的有序处理,而底层的MediaExtractor则提供了高效的数据读取能力。
轨道分离机制详解
1. 轨道管理核心结构
kotlin
internal class Tracks(
strategies: TrackMap<TrackStrategy>, // 音视频轨道策略
sources: DataSources, // 数据源
videoRotation: Int, // 视频旋转角度
forceCompression: Boolean // 是否强制压缩
) {
val all: TrackMap<TrackStatus> // 所有轨道状态
val outputFormats: TrackMap<MediaFormat> // 输出格式
val active: TrackMap<TrackStatus> // 活动轨道状态
}
2. 轨道解析过程
2.1 初始化流程
scss
init {
// 1. 解析音频轨道
val (audioFormat, audioStatus) = resolveTrack(
TrackType.AUDIO,
strategies.audio,
sources.audioOrNull()
)
// 2. 解析视频轨道
val (videoFormat, videoStatus) = resolveTrack(
TrackType.VIDEO,
strategies.video,
sources.videoOrNull()
)
// 3. 确定最终状态
all = trackMapOf(
video = resolveVideoStatus(videoStatus, forceCompression, videoRotation),
audio = resolveAudioStatus(audioStatus, forceCompression)
)
// 4. 设置输出格式
outputFormats = trackMapOf(
video = videoFormat,
audio = audioFormat
)
}
2.2 轨道解析详细过程
kotlin
private fun resolveTrack(
type: TrackType,
strategy: TrackStrategy,
sources: List<DataSource>?
): Pair<MediaFormat, TrackStatus> {
// 1. 空轨道检查
if (sources == null) {
return MediaFormat() to TrackStatus.ABSENT
}
// 2. 创建格式提供器
val provider = MediaFormatProvider()
// 3. 提取有效输入格式
val inputs = sources.mapNotNull { source ->
val format = source.getTrackFormat(type) ?: return@mapNotNull null
provider.provideMediaFormat(source, type, format)
}
// 4. 根据输入格式数量处理
return when (inputs.size) {
0 -> MediaFormat() to TrackStatus.ABSENT
sources.size -> {
val output = MediaFormat()
val status = strategy.createOutputFormat(inputs, output)
output to status
}
else -> error("轨道不一致错误")
}
}
3. 轨道状态管理
3.1 视频轨道状态解析
kotlin
private fun resolveVideoStatus(
status: TrackStatus,
forceCompression: Boolean,
rotation: Int
): TrackStatus {
val force = forceCompression || rotation != 0
val canForce = status == TrackStatus.PASS_THROUGH
return if (canForce && force) {
TrackStatus.COMPRESSING
} else {
status
}
}
3.2 音频轨道状态解析
kotlin
private fun resolveAudioStatus(
status: TrackStatus,
forceCompression: Boolean
): TrackStatus {
val force = forceCompression
val canForce = status == TrackStatus.PASS_THROUGH
return if (canForce && force) {
TrackStatus.COMPRESSING
} else {
status
}
}
3.3 活动轨道管理
css
val active: TrackMap<TrackStatus> = trackMapOf(
video = all.video.takeIf { it.isTranscoding },
audio = all.audio.takeIf { it.isTranscoding }
)
4. 轨道分离特点
- 独立状态管理
-
每个轨道类型(音频/视频)有独立的状态
-
状态包括:ABSENT(不存在)、PASS_THROUGH(直接通过)、COMPRESSING(压缩中)
- 格式分离
ini
val outputFormats: TrackMap<MediaFormat> = trackMapOf(
video = videoFormat,
audio = audioFormat
)
- 策略分离
-
每个轨道类型使用独立的转码策略
-
策略决定输出格式和处理方式
- 数据源验证
rust
when (inputs.size) {
0 -> MediaFormat() to TrackStatus.ABSENT
sources.size -> // 正常处理
else -> error("轨道不一致错误")
}
5. 轨道处理流程
css
[输入源]
│
├── 音频轨道
│ ├── 状态解析
│ │ ├── ABSENT(不存在)
│ │ ├── PASS_THROUGH(直接通过)
│ │ └── COMPRESSING(压缩中)
│ │
│ └── 格式处理
│ ├── 输入格式提取
│ └── 输出格式创建
│
└── 视频轨道
├── 状态解析
│ ├── ABSENT(不存在)
│ ├── PASS_THROUGH(直接通过)
│ └── COMPRESSING(压缩中)
│
└── 格式处理
├── 输入格式提取
└── 输出格式创建
6. 轨道分离的优势
- 灵活性
-
音视频轨道可以独立处理
-
支持不同的处理策略
- 可控性
-
精确控制每个轨道的状态
-
支持强制压缩选项
- 健壮性
-
严格的轨道验证
-
完善的错误处理
- 效率
-
只处理活动轨道
-
避免不必要的转码
这种轨道分离机制确保了音视频处理的独立性和灵活性,同时通过严格的状态管理和格式处理保证了转码过程的可靠性。
视频转码压缩过程分析
以下将重新梳理视频转码和压缩的实现原理,结合已分析的代码文件进行详细阐述
1. 压缩策略设计
DefaultVideoStrategy.java
的核心作用
该类是视频压缩策略的核心实现类,主要从三个关键维度进行视频压缩:
- 分辨率调整(尺寸压缩) :借助
Resizer
系列类(如ExactResizer
、AtMostResizer
等)计算目标分辨率,确保输入和输出视频的宽高比匹配,从而实现视频尺寸的压缩。
DefaultVideoStrategy.java
Size outSize = options.resizer.getOutputSize(inSize);
- 比特率控制(数据量压缩) :通过
BitRates
工具类,依据视频的分辨率和帧率估算合适的比特率。若未指定目标比特率,会自动估算;若指定,则使用指定值。
DefaultVideoStrategy.java
int outBitRate = (int) (options.targetBitRate == BITRATE_UNKNOWN
? BitRates.estimateVideoBitRate(outWidth, outHeight, outFrameRate)
: options.targetBitRate);
- 帧率控制(时间维度压缩) :在
createOutputFormat
方法中,比较输入帧率和目标帧率,取较小值作为输出帧率,实现时间维度的压缩。
DefaultVideoStrategy.java
int outFrameRate = Math.min(inputFrameRate, options.targetFrameRate);
2. 转码流程
rust
plainText
输入 -> 解码 -> 处理 -> 编码 -> 输出
- 输入 :
DataSources
负责读取原始视频数据,为后续处理提供数据来源。 - 解码 :在
RegularPipeline.kt
中,使用MediaCodec
将压缩的视频数据解码为原始的视频帧。 - 处理 :应用
DefaultVideoStrategy
中的压缩策略,进行分辨率调整、比特率控制和帧率控制。 - 编码 :同样在
RegularPipeline.kt
中,使用MediaCodec
将处理后的视频帧重新编码为压缩格式。 - 输出 :
DataSink
把编码后的视频数据写入到目标文件或流中。
3. 压缩核心实现
分辨率压缩
- 计算目标分辨率 :通过
Resizer
系列类根据输入尺寸和预设策略计算目标分辨率。 - 图像缩放 :在
RegularPipeline.kt
里,使用 OpenGL ES 进行图像缩放,实现分辨率的调整,同时支持硬件加速。
比特率控制
- 估算比特率 :
BitRates.java
工具类依据视频的分辨率和帧率估算最佳比特率,在保证视频质量的前提下,尽量减小文件大小。
BitRates.java
public static int estimateVideoBitRate(int width, int height, int frameRate) {
// 具体的估算逻辑
}
帧率控制
- 时间戳处理 :通过
TimeInterpolator
控制视频帧的时间戳,在降低帧率的同时保证视频的流畅性。
4. 优化机制
硬件加速
- 编解码加速 :优先使用硬件编解码器,在
RegularPipeline.kt
中对MediaCodec
的使用体现了对硬件加速的支持。 - 图像处理加速:借助 OpenGL ES 进行图像缩放,利用 GPU 加速图像处理过程。
智能跳过
- 透传判断 :在
DefaultVideoStrategy.java
的createOutputFormat
方法中,通过Validator
逻辑判断输入视频是否符合目标参数,若符合则直接透传,无需重新编码。
分段处理
- 视频分段 :
Segments.kt
将视频数据进行分段处理,支持并行处理,提高转码效率。
Segments.kt
fun hasNext(type: TrackType): Boolean {
// 判断是否还有下一个分段
}
5. 关键类的职责
DefaultTranscodeEngine.kt
:作为转码引擎的实现类,负责协调各个模块,控制整个转码流程。Tracks
:管理音视频轨道,确保音视频数据的正确处理。Segments
:处理视频数据的分段,实现分段处理和并行处理。Codecs
:管理编解码器,负责编解码操作的初始化和执行。Timer
:控制转码处理的进度和视频帧的时间戳。Pipeline
:处理视频数据流的转换,包括解码、处理和编码等操作。
6. 压缩效果控制
- 质量与大小平衡:通过动态调整比特率和分辨率,结合源视频的特征选择最佳参数,实现视频质量和文件大小的平衡。
- 自定义参数 :提供
DefaultVideoStrategy.Builder
类,允许用户自定义比特率、帧率、关键帧间隔等压缩参数。
DefaultVideoStrategy.java
DefaultVideoStrategy strategy = DefaultVideoStrategy.exact(1280, 720)
.bitRate(2000000)
.frameRate(25)
.build();
压缩过程流程图
实现方案优点
- 灵活的压缩策略 :支持多种
Resizer
类,可根据不同需求调整分辨率,同时允许自定义比特率和帧率。 - 硬件加速支持:利用硬件编解码器和 OpenGL ES 实现编解码和图像处理的加速。
- 智能参数选择:自动估算比特率,根据输入视频特征选择合适的压缩参数。
- 高效的分段处理 :通过
Segments
实现视频分段和并行处理,提高转码效率。 - 可定制的压缩参数:提供构建器模式,方便用户自定义压缩参数。
Pipeline(流水线)
在编程领域,Pipeline
(管道)是一种常见的设计概念,它可以类比为现实生活中的工业生产流水线,下面通过具体的例子来形象说明。
现实生活中的流水线
想象有一个汽车制造工厂,汽车的生产过程要经过多个步骤,比如焊接车身、安装发动机、喷漆、安装内饰等。每个步骤都有专门的工人或机器负责,汽车在流水线上依次经过这些步骤,最终变成一辆完整的成品车。
编程中的 Pipeline
在编程里,Pipeline
就类似于这个汽车制造流水线。它把一系列的处理步骤按照顺序组合起来,数据从一端输入,依次经过每个处理步骤,最终得到处理后的结果。在你当前编辑的 Kotlin 文件里,Pipeline
用于处理音视频数据的转码,下面结合代码详细说明。
代码中的 Pipeline
示例
视频管道 VideoPipeline
pipelines.kt
private fun VideoPipeline(
debug: String?,
source: DataSource,
sink: DataSink,
interpolator: TimeInterpolator,
format: MediaFormat,
codecs: Codecs,
videoRotation: Int
) = Pipeline.build("Video", debug) {
// 从数据源读取视频轨道的数据
Reader(source, TrackType.VIDEO) +
// 对读取的视频数据进行解码
Decoder(source.getTrackFormat(TrackType.VIDEO)!!, true) +
// 使用时间插值器调整解码后视频数据的时间戳
DecoderTimer(TrackType.VIDEO, interpolator) +
// 对解码后的视频帧进行渲染,考虑视频的方向和旋转角度
VideoRenderer(source.orientation, videoRotation, format) +
// 将渲染后的视频帧发布出去
VideoPublisher() +
// 对处理后的视频数据进行编码
Encoder(codecs, TrackType.VIDEO) +
// 将编码后的视频数据写入数据接收器
Writer(sink, TrackType.VIDEO)
}
在这个视频管道里,每个组件就像是流水线上的一个工位:
Reader
:相当于原材料供应商,负责从数据源读取原始的视频数据。Decoder
:好比把原材料进行初步加工的工人,将压缩的视频数据解码为原始的视频帧。DecoderTimer
:类似调整生产节奏的控制器,使用时间插值器调整视频帧的时间戳。VideoRenderer
:如同给汽车喷漆和装饰的工人,对视频帧进行渲染,调整视频的方向和旋转角度。VideoPublisher
:可看作是将半成品传递到下一个工位的传送带,把渲染后的视频帧发布出去。Encoder
:像是把半成品组装成成品的工人,对处理后的视频帧进行编码,压缩成目标格式。Writer
:就像成品包装和发货的工人,将编码后的视频数据写入数据接收器。
总结
Pipeline
是一种将多个处理步骤串联起来的设计模式,它让数据能按照预定的顺序依次经过各个处理步骤,最终得到期望的结果。这种模式提高了代码的可维护性和可扩展性,因为每个处理步骤都可以独立开发、测试和修改。
Pipeline包下的类
pipeline
包下的类主要用于构建和管理数据处理管道,这些管道会按顺序处理音视频数据。下面详细介绍包内主要类的功能以及它们的协作方式。
主要类及其功能
1. Pipeline.kt
Pipeline
类 :表示一个完整的数据处理管道,将多个Step
组合在一起,按顺序执行。它包含execute
方法用于执行管道处理逻辑,release
方法用于释放资源。PipelineItem
类 :是Pipeline
内部类,代表管道中的一个处理项,封装了一个Step
及其相关状态,如是否完成、是否推进等。Builder
类 :用于构建Pipeline
,支持通过+
运算符将多个Step
连接起来。
2. State.kt
此文件虽未给出完整内容,但从代码引用可知,State
是密封类,用于表示处理步骤的状态,常见状态有 State.Ok
(处理成功)、State.Retry
(需要重试)、State.Eos
(处理结束)等。
3. Step.kt
Step
接口 :定义了处理步骤的基本行为,包含initialize
方法用于初始化,advance
方法用于推进处理流程。
4. pipelines.kt
EmptyPipeline
函数:创建一个空的处理管道。PassThroughPipeline
函数:创建一个直接传递数据的管道,不做额外处理。RegularPipeline
函数:根据轨道类型(音频或视频)创建对应的处理管道。AudioPipeline
函数:创建音频处理管道,包含读取、解码、时间调整、音频处理、编码和写入等步骤。VideoPipeline
函数:创建视频处理管道,包含读取、解码、时间调整、渲染、发布、编码和写入等步骤。
5. steps.kt
BaseStep
抽象类 :处理步骤的基类,实现了Step
接口,提供了日志记录和初始化下一个通道的功能。TransformStep
抽象类 :继承自BaseStep
,用于对输入数据进行转换处理,输入和输出类型相同。QueuedStep
抽象类 :继承自BaseStep
,用于处理需要排队的输入数据,提供了数据入队、处理结束标志入队和数据出队的抽象方法。
类之间的配合方式
- 构建管道 :使用
Pipeline.Builder
类将多个Step
组合成一个Pipeline
。例如,在VideoPipeline
函数中,通过+
运算符将Reader
、Decoder
等多个Step
连接起来,再调用Pipeline.build
方法创建Pipeline
实例。
pipelines.kt
private fun VideoPipeline(
debug: String?,
source: DataSource,
sink: DataSink,
interpolator: TimeInterpolator,
format: MediaFormat,
codecs: Codecs,
videoRotation: Int
) = Pipeline.build("Video", debug) {
Reader(source, TrackType.VIDEO) +
Decoder(source.getTrackFormat(TrackType.VIDEO)!!, true) +
// 其他步骤...
}
- 初始化管道 :
Pipeline
类的init
块会将PipelineItem
按顺序连接起来,调用attachToNext
方法将每个Step
与下一个Step
的通道关联起来。
Pipeline.kt
internal class Pipeline private constructor(name: String, private val items: List<PipelineItem>) {
init {
items.zipWithNext().reversed().forEach { (first, next) -> first.attachToNext(next) }
}
// ...
}
- 执行管道 :调用
Pipeline
的execute
方法,按顺序遍历每个PipelineItem
,检查是否可以处理数据,若可以则调用handle
方法推进处理流程。处理结果会根据State
类型进行不同处理,如成功则传递给下一个PipelineItem
,失败则根据情况重试或暂停。
Pipeline.kt
fun execute(): State<Unit> {
for (i in items.indices) {
val item = items[i]
if (item.canHandle(i == 0)) {
val failure = item.handle()
// 处理失败情况...
}
}
// 返回最终状态...
}
- 释放资源 :处理完成后,调用
Pipeline
的release
方法,遍历每个PipelineItem
,调用其Step
的release
方法释放资源。
Pipeline.kt
fun release() {
items.forEach { it.step.release() }
}
综上所述,pipeline
包下的类通过构建、初始化、执行和释放资源等步骤,协同工作完成音视频数据的处理。
有限状态集合语言层支持
State
类采用密封接口和类的设计,这种设计在 Kotlin 里常用于表示有限的状态集合,下面从几个方面分析为何这样设计:
1. 密封接口与类的使用
State
被定义为密封接口,这意味着所有实现该接口的类都必须在同一文件内声明。这样做的好处是编译器能知晓所有可能的实现类,在进行 when
表达式分支判断时,若覆盖了所有实现类,就无需再添加 else
分支,增强了类型安全性。
2. 状态分类
State
接口使用泛型 <out T>
,代表该接口是协变的,即 State<SubType>
可以作为 State<SuperType>
使用。它将状态分为以下几类:
正常运行状态 Ok
kotlin
open class Ok<T>(val value: T) : State<T> {
override fun toString() = "State.Ok($value)"
}
Ok
类表示正常运行状态,包含一个泛型值 value
,代表操作的正常结果。open
关键字允许其他类继承它。
结束状态 Eos
kotlin
class Eos<T>(value: T) : Ok<T>(value) {
override fun toString() = "State.Eos($value)"
}
Eos
(End of Stream)类继承自 Ok
类,表示操作运行到最后一次,同样包含操作结果 value
。
失败状态 Failure
kotlin
sealed interface Failure : State<Nothing> {
val sleep: Boolean
}
Failure
是一个密封接口,继承自 State<Nothing>
,表示操作失败的状态。Nothing
类型代表该状态不包含任何有意义的值。sleep
属性用于指示在失败后是否需要暂停一段时间再重试。
重试状态 Retry
和消费状态 Consume
kotlin
class Retry(override val sleep: Boolean) : Failure {
override fun toString() = "State.Retry($sleep)"
}
class Consume(override val sleep: Boolean = false) : Failure {
override fun toString() = "State.Consume($sleep)"
}
Retry
类表示操作失败,需要重试,sleep
属性决定是否暂停。Consume
类也表示操作失败,但通常用于需要消费某些资源的场景,sleep
默认值为 false
。
3. 设计目的
- 状态管理:通过定义不同的状态类,能清晰地表示操作在不同阶段的状态,方便代码的管理和维护。
- 类型安全:密封接口和类的使用让编译器能在编译时检查状态处理的完整性,避免遗漏某些状态。
- 可扩展性 :由于
Ok
类是open
的,后续可以继承它来扩展更多的正常运行状态。同时,Failure
是密封接口,也能方便地添加新的失败状态。
综上所述,当前类的设计是为了高效、安全地管理操作的不同状态,提高代码的可读性和可维护性。
MediaMuxer输出
在 Android 开发里,MediaMuxer
类用于将编码后的音视频数据混合到一个容器文件中,常见的输出格式有 MP4、WebM 等。DefaultDataSink
类就运用了 MediaMuxer
来生成可读的媒体文件。下面详细介绍 MediaMuxer
的使用步骤和 DefaultDataSink
类中的具体实现。
MediaMuxer
使用步骤
1. 创建 MediaMuxer
实例
借助文件路径或者文件描述符创建 MediaMuxer
对象,同时指定输出格式。
java
try {
// 使用文件路径创建 MediaMuxer,输出格式为 MPEG-4
MediaMuxer mMuxer = new MediaMuxer("output.mp4", MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
e.printStackTrace();
}
在 DefaultDataSink
类中,构造方法实现了该功能:
DefaultDataSink.java
public DefaultDataSink(@NonNull String outputFilePath) {
this(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
@SuppressWarnings("WeakerAccess")
public DefaultDataSink(@NonNull String outputFilePath, int format) {
try {
mMuxer = new MediaMuxer(outputFilePath, format);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
2. 设置元数据(可选)
可设置输出文件的方向和地理位置等元数据。
java
// 设置视频旋转角度
mMuxer.setOrientationHint(90);
// 设置地理位置
mMuxer.setLocation(30.0, 120.0);
在 DefaultDataSink
类中,对应方法如下:
DefaultDataSink.java
@Override
public void setOrientation(int rotation) {
mMuxer.setOrientationHint(rotation);
}
@Override
public void setLocation(double latitude, double longitude) {
mMuxer.setLocation((float) latitude, (float) longitude);
}
3. 添加轨道
在启动 MediaMuxer
之前,要添加音视频轨道,并获取轨道索引。
java
// 创建视频轨道的 MediaFormat
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720);
// 添加视频轨道并获取索引
int videoTrackIndex = mMuxer.addTrack(videoFormat);
// 创建音频轨道的 MediaFormat
MediaFormat audioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 2);
// 添加音频轨道并获取索引
int audioTrackIndex = mMuxer.addTrack(audioFormat);
在 DefaultDataSink
类中,maybeStart
方法实现了轨道添加:
DefaultDataSink.java
private void maybeStart() {
// ...已有代码...
if (isTranscodingVideo) {
int videoIndex = mMuxer.addTrack(videoOutputFormat);
mMuxerIndex.setVideo(videoIndex);
LOG.v("Added track #" + videoIndex + " with " + videoOutputFormat.getString(MediaFormat.KEY_MIME) + " to muxer");
}
if (isTranscodingAudio) {
int audioIndex = mMuxer.addTrack(audioOutputFormat);
mMuxerIndex.setAudio(audioIndex);
LOG.v("Added track #" + audioIndex + " with " + audioOutputFormat.getString(MediaFormat.KEY_MIME) + " to muxer");
}
// ...已有代码...
}
4. 启动 MediaMuxer
添加完所有轨道后,启动 MediaMuxer
。
java
mMuxer.start();
在 DefaultDataSink
类的 maybeStart
方法中也有启动操作:
DefaultDataSink.java
private void maybeStart() {
// ...已有代码...
mMuxer.start();
mMuxerStarted = true;
drainQueue();
}
5. 写入数据
启动 MediaMuxer
后,将编码后的音视频数据写入对应的轨道。
java
// 假设 byteBuffer 是编码后的数据,bufferInfo 是数据信息
mMuxer.writeSampleData(videoTrackIndex, byteBuffer, bufferInfo);
在 DefaultDataSink
类的 writeTrack
方法中实现了数据写入:
DefaultDataSink.java
@Override
public void writeTrack(@NonNull TrackType type, @NonNull ByteBuffer byteBuffer, @NonNull MediaCodec.BufferInfo bufferInfo) {
if (mMuxerStarted) {
if (bufferInfo.presentationTimeUs != 0) {
bufferInfo.presentationTimeUs = mInterpolator.interpolate(type, bufferInfo.presentationTimeUs);
}
mMuxer.writeSampleData(mMuxerIndex.get(type), byteBuffer, bufferInfo);
} else {
enqueue(type, byteBuffer, bufferInfo);
}
}
6. 停止和释放资源
数据写入完成后,停止 MediaMuxer
并释放资源。
java
mMuxer.stop();
mMuxer.release();
在 DefaultDataSink
类中,对应方法如下:
DefaultDataSink.java
@Override
public void stop() {
mMuxer.stop(); // If this fails, let's throw.
}
@Override
public void release() {
try {
mMuxer.release();
} catch (Exception e) {
LOG.w("Failed to release the muxer.", e);
}
}
总结
MediaMuxer
的使用流程主要包括创建实例、设置元数据、添加轨道、启动、写入数据以及停止和释放资源。DefaultDataSink
类完整实现了这些步骤,保证了转码后的数据能正确混合到输出文件中。
Android MediaCodec API 的详细使用指南
一、MediaCodec 基础概念
1. MediaCodec 是什么?
- Android 提供的 底层音视频编解码 API,直接调用硬件加速(如 H.264/H.265 编码、AAC 解码)。
- 支持 编码(Encoder) 和 解码(Decoder) 两种模式。
- 适用于 视频转码、实时流媒体、摄像头录制、屏幕录制 等场景。
2. 核心组件
组件 | 作用 |
---|---|
MediaCodec | 编解码器核心类,负责处理音视频数据流。 |
MediaFormat | 描述音视频的格式(如分辨率、帧率、编码类型)。 |
BufferInfo | 描述数据缓冲区的元信息(如时间戳、大小、偏移量)。 |
Input/Output Buffer | 数据输入/输出的缓冲区,需手动管理(或使用 queueInputBuffer /dequeueOutputBuffer )。 |
二、MediaCodec 使用流程
1. 编码流程(以视频 H.264 编码为例)
步骤 1:配置 MediaCodec
scss
val mime = "video/avc" // H.264
val width = 1280
val height = 720
val frameRate = 30
val bitrate = 4000000 // 4Mbps
val format = MediaFormat.createVideoFormat(mime, width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) // 使用 Surface 输入
setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) // 关键帧间隔
}
val codec = MediaCodec.createEncoderByType(mime)
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
val inputSurface = codec.createInputSurface() // 创建输入 Surface(用于摄像头或 SurfaceTexture)
codec.start()
步骤 2:输入数据(YUV 或 Surface)
-
方式 1:直接写入 YUV 数据
scssval inputBufferIndex = codec.dequeueInputBuffer(timeoutUs) if (inputBufferIndex >= 0) { val buffer = codec.getInputBuffer(inputBufferIndex)!! buffer.put(yuvData) // 填充 YUV 数据 codec.queueInputBuffer(inputBufferIndex, 0, yuvData.size, presentationTimeUs, flags) }
-
方式 2:通过 Surface 输入(推荐摄像头使用)
arduino// 将摄像头数据渲染到 inputSurface(通过 SurfaceTexture 或 OpenGL)
步骤 3:获取编码输出
ini
val outputBufferInfo = MediaCodec.BufferInfo()
while (true) {
val outputBufferIndex = codec.dequeueOutputBuffer(outputBufferInfo, timeoutUs)
if (outputBufferIndex >= 0) {
val buffer = codec.getOutputBuffer(outputBufferIndex)!!
// 处理编码后的数据(如写入文件或传输)
codec.releaseOutputBuffer(outputBufferIndex, false)
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 格式变化时获取新的 MediaFormat(如 SPS/PPS)
val newFormat = codec.outputFormat
}
}
步骤 4:释放资源
scss
codec.stop()
codec.release()
inputSurface.release()
2. 解码流程(以 H.264 解码为例)
步骤 1:配置 MediaCodec
java
val mime = "video/avc"
val format = MediaFormat.createVideoFormat(mime, width, height)
format.setByteBuffer("csd-0", ByteBuffer.wrap(spsData)) // SPS 数据
format.setByteBuffer("csd-1", ByteBuffer.wrap(ppsData)) // PPS 数据
val codec = MediaCodec.createDecoderByType(mime)
codec.configure(format, surfaceView.holder.surface, null, 0)
codec.start()
步骤 2:输入编码数据
scss
val inputBufferIndex = codec.dequeueInputBuffer(timeoutUs)
if (inputBufferIndex >= 0) {
val buffer = codec.getInputBuffer(inputBufferIndex)!!
buffer.put(encodedData) // 填充 H.264 数据
codec.queueInputBuffer(inputBufferIndex, 0, encodedData.size, presentationTimeUs, flags)
}
步骤 3:获取解码输出
java
val outputBufferInfo = MediaCodec.BufferInfo()
while (true) {
val outputBufferIndex = codec.dequeueOutputBuffer(outputBufferInfo, timeoutUs)
if (outputBufferIndex >= 0) {
val buffer = codec.getOutputBuffer(outputBufferIndex)!!
// 渲染到 Surface 或处理解码后的帧
codec.releaseOutputBuffer(outputBufferIndex, true) // true 表示渲染到 Surface
}
}
步骤 4:释放资源
scss
codec.stop()
codec.release()
三、关键注意事项
1. 缓冲区管理
-
输入缓冲区:需手动填充数据(YUV 或 Surface)。
-
输出缓冲区 :需检查
BufferInfo
的flags
:MediaCodec.BUFFER_FLAG_END_OF_STREAM
:流结束。MediaCodec.BUFFER_FLAG_CODEC_CONFIG
:包含 SPS/PPS 等配置信息。
2. 同步与延迟
- 编码延迟:硬件编码可能有几帧延迟(需缓冲)。
- 解码延迟:首次解码可能需要等待 SPS/PPS。
3. 错误处理
-
检查
dequeueInputBuffer
/dequeueOutputBuffer
返回值:< 0
:错误或需要等待(如INFO_TRY_AGAIN_LATER
)。INFO_OUTPUT_FORMAT_CHANGED
:格式变化(需重新获取outputFormat
)。
4. 性能优化
- 使用 Surface 输入 :摄像头数据直接渲染到
inputSurface
(避免 CPU 拷贝)。 - 合理设置缓冲区大小:避免频繁分配/释放内存。
四、完整代码示例(视频编码)
kotlin
class VideoEncoder(private val width: Int, private val height: Int) {
private var codec: MediaCodec? = null
private var inputSurface: Surface? = null
fun start() {
val mime = "video/avc"
val format = MediaFormat.createVideoFormat(mime, width, height).apply {
setInteger(MediaFormat.KEY_BIT_RATE, 4000000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
}
codec = MediaCodec.createEncoderByType(mime)
codec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
inputSurface = codec?.createInputSurface()
codec?.start()
}
fun encodeFrame(yuvData: ByteArray, presentationTimeUs: Long) {
val inputBufferIndex = codec?.dequeueInputBuffer(10000) ?: return
if (inputBufferIndex >= 0) {
val buffer = codec?.getInputBuffer(inputBufferIndex)!!
buffer.put(yuvData)
codec?.queueInputBuffer(inputBufferIndex, 0, yuvData.size, presentationTimeUs, 0)
}
}
fun stop() {
codec?.stop()
codec?.release()
inputSurface?.release()
}
}
五、常见问题解答
Q1:如何获取 SPS/PPS 数据?
-
在
dequeueOutputBuffer
返回INFO_OUTPUT_FORMAT_CHANGED
时,通过codec.outputFormat
获取:inival sps = codec.outputFormat.getByteBuffer("csd-0").array() val pps = codec.outputFormat.getByteBuffer("csd-1").array()
Q2:如何降低编码延迟?
- 减少
BIT_RATE
或调整FRAME_RATE
。 - 使用
BUFFER_FLAG_KEY_FRAME
控制关键帧间隔。
Q3:如何调试 MediaCodec?
-
打印
BufferInfo
和MediaFormat
信息:bashLog.d("MediaCodec", "Output format: ${codec.outputFormat}") Log.d("MediaCodec", "Buffer info: $outputBufferInfo")
六、进阶方向
- 结合 OpenGL ES :通过
SurfaceTexture
实现高效图像处理(如滤镜)。 - 多线程编码:分离输入/输出线程避免阻塞。
- 硬件编解码器选择 :通过
MediaCodecList
查询设备支持的编解码器。
MediaCodec 是 Android 音视频处理的核心,掌握它能实现高性能的转码、直播、录制等功能! 🚀