在这篇博文中, 我们将讨论 Kotlin/Native 应用程序的开发在这里, 我们使用 FFMPEG 音频 / 视频解码器和 SDL2 进行渲染, 来开发个简易的视频播放器希望此文可以成为能对 Kotlin/Native 开发者有价值的开发指南, 同时该文也会解释使用该平台的预期机制
在本教程中, 我们主要关注的是 Kotlin/Native, 我们只会粗略地介绍一下如何开发视频层您可以参阅这篇名为如何用不到 1000 行代码编写一个视频播放器的优秀教程, 以了解如何用 C 语言实现它如果您的兴趣点在于比较 C 语言的编码与 Kotlin/Native 编码的不同之处, 我建议您从本教程开始
理论上, 每一个视频播放器的工作都相当简单: 读入带有交错的视频帧和音频帧的输入流, 解码并显示视频帧, 同时与音频流同步通常, 这一工作由多个线程完成, 执行流解码播放视频和音频要准确的做到这些, 需要线程同步和特定的实时保证, 如果音频流没有被及时解码, 播放声音听起来会很不稳定, 如果视频帧没有及时显示, 影像看起来会很不流畅
罗辑思维 Go 语言微服务改造完整过程 Netflix 的未来 IT 架构模型: Serverless 阿里巴巴数据处理引擎 Blink 核心设计 谷歌研究院出品: TensorFlow 在深度学习中的应用
Kotlin/Native 不鼓励您使用线程, 也不提供在线程之间共享 Kotlin 对象的方法然而, 我们相信在 Kotlin/Native 中并发的软实时编程很容易实现, 所以我们决定从一开始就以并发的方式来设计我们的播放器来看看我们是怎么做到的吧
Kotlin/Native 计算并发性是围绕 workers 构建的 Worker 是比线程更高级的并发性概念, 不像对象共享和同步, 它允许对象传输, 因此每一时刻只有一个 workers 可以访问特定对象这意味着, 访问对象数据时不需要同步, 因为多个访问永远不能同时进行 workers 可以接收执行请求, 这些请求可以接受对象并根据需要执行任务, 然后将结果返回给需要计算结果的人这样的模型确保了许多典型的并发编程错误 (例如对共享数据的不同步访问, 或者由未排序的锁导致的死锁) 不再出现
让我们看看, 它是如何转化为视频播放器架构的我们需要对某些容器格式进行解码, 比如 avi.mkv 或者 .mpg, 它对交叉音频和视频流进行多路分解解码, 然后将解压缩的音频提供给 SDL 音频线程解压后的视频帧应与声音播放同步为了达到这个目标, worker 概念的出现也便是理所当然的了我们为解码器生成一个 worker, 并在需要的时候向它请求视频和音频数据在多核机器上, 这意味着解码可以与播放并行进行因此, 解码器是一个来自 UI 线程和音频线程的数据生成器
无论何时我们需要获取下一个音频或视频数据块时, 我们都依赖于全能的 schedule()函数它将调度大量的工作给特定的 worker 执行, 以便提供输入参数和返回 Future 实例, 这些可能被挂起, 直到任务被目标 worker 执行完 Future 对象可能被销毁, 因此产生的对象将直接从 worker 线程返回到请求程序线程
Kotlin/Native 运行时理论上讲是线性的, 所以当运行多个线程时, 需要在做其他操作之前调用函数 konan.initRuntimeIfNeeded(), 我们在音频线程回调中也是这样做的为了简化音频播放, 我们将音频帧重新采样到两个通道, 并以 44100 的采样率对 16 位整数流进行标识
视频帧可以被解码成用户需要的大小, 当然它会有个默认值, 同时它的位深度依赖于用户桌面默认设置还请注意下 Kotlin/Native 特有的操作 C 指针的方法, 即
- private val resampledAudioFrame: AVFrame =
- disposable(create = ::av_frame_alloc, dispose = ::av_frame_unref).pointed
- ...
- with (resampledAudioFrame) {
- channels = output.channels
- sample_rate = output.sampleRate
- format = output.sampleFormat
- channel_layout = output.channelLayout.signExtend()
- }
我们声明 resampledAudioFrame 作为由 FFMPEG API 调用 avframealloc()和 avframeunref()创建的 C 程序中的一次性资源然后, 我们将它所指向的值设置成它所期望的字段需要注意的是, 我们可以将 FFMPEG(如 AV_PIX_FMT_RGB24)声明的定义作为 Kotlin 的常量但是, 由于它们没有类型信息, 并且默认情况下是 Int 类型的, 如果某个字段具有不同的类型 (比如 channellayout), 那便需要调用适配器函数 signExtend() 这是编译器的内在特性, 它会插入适当的转换中
在设置完解码器后, 我们开始播放流程这没有什么特别的, 只是检索下一个帧, 将它呈现给纹理, 并在屏幕上显示这个纹理至此, 视频帧便被渲染了音频线程回调是由音频线程回调处理的, 它从解码器中获取下一个采样缓冲区, 并将其反馈给音频引擎
音频 / 视频同步是必须要保证的, 它可以确保我们没有太多的未播放的音频帧真正的多媒体播放器应该依赖于帧时间戳, 我们只计算它, 但永远不会使用这里有一个有趣的地方
- val ts = av_frame_get_best_effort_timestamp(audioFrame.ptr) *
- av_q2d(audioCodecContext.time_base.readValue())
它展示了如何使用 api 接收 C 语言的结构体它是在 libavutil/rational.h 中声明的
- static inline double av_q2d(AVRational a){
- return a.num / (double) a.den;
- }
因此, 要通过值传递它, 我们首先需要在字段上使用 readValue()
总结来说, 多亏了 FFMPEG 库, 我们才能用较少的代价便实现了一个支持多种输入格式的简易音频 / 视频播放器这里我们还讨论了 Kotlin/Native 中基于 C 语言的互操作性相关的基础知识, 以及更容易使用和维护的并发方法
英文原文链接: Application development in Kotlin/Native
来源: http://www.infoq.com/cn/articles/application-development-in-kotlinnative