Native: move samples to backend.native/tests/

This commit is contained in:
Svyatoslav Scherbina
2022-08-26 11:50:39 +02:00
committed by Space
parent b7337d2e64
commit 7bf6d64cfb
94 changed files with 94 additions and 93 deletions
@@ -0,0 +1,7 @@
ffmpeg and SDL2 are needed for the compilation, i.e.
port install ffmpeg-devel
brew install ffmpeg sdl2
apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
apt install libsdl2-dev
pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-ffmpeg
@@ -0,0 +1,56 @@
plugins {
kotlin("multiplatform")
}
val mingwPath = File(System.getenv("MINGW64_DIR") ?: "C:/msys64/mingw64")
kotlin {
// Determine host preset.
val hostOs = System.getProperty("os.name")
// Create target for the host platform.
val hostTarget = when {
hostOs == "Mac OS X" -> macosX64("videoPlayer")
hostOs == "Linux" -> linuxX64("videoPlayer")
hostOs.startsWith("Windows") -> mingwX64("videoPlayer")
else -> throw GradleException("Host OS '$hostOs' is not supported in Kotlin/Native $project.")
}
hostTarget.apply {
binaries {
executable {
entryPoint = "sample.videoplayer.main"
when (preset) {
presets["macosX64"] -> linkerOpts("-L/opt/local/lib", "-L/usr/local/lib")
presets["linuxX64"] -> linkerOpts("-L/usr/lib/x86_64-linux-gnu", "-L/usr/lib64")
presets["mingwX64"] -> linkerOpts("-L${mingwPath.resolve("lib")}")
}
}
}
compilations["main"].cinterops {
val ffmpeg by creating {
when (preset) {
presets["macosX64"] -> includeDirs.headerFilterOnly("/opt/local/include", "/usr/local/include")
presets["linuxX64"] -> includeDirs.headerFilterOnly("/usr/include", "/usr/include/x86_64-linux-gnu", "/usr/include/ffmpeg")
presets["mingwX64"] -> includeDirs(mingwPath.resolve("include"))
}
}
val sdl by creating {
when (preset) {
presets["macosX64"] -> includeDirs("/opt/local/include/SDL2", "/usr/local/include/SDL2")
presets["linuxX64"] -> includeDirs("/usr/include", "/usr/include/x86_64-linux-gnu", "/usr/include/SDL2")
presets["mingwX64"] -> includeDirs(mingwPath.resolve("include/SDL2"))
}
}
}
compilations["main"].enableEndorsedLibs = true
}
// Enable experimental stdlib API used by the sample.
sourceSets.all {
languageSettings.optIn("kotlin.ExperimentalStdlibApi")
}
}
@@ -0,0 +1,2 @@
kotlin.code.style=official
kotlin.import.noCommonSourceSets=true
@@ -0,0 +1,27 @@
package = ffmpeg
headers = libavcodec/avcodec.h libavformat/avformat.h libavutil/pixfmt.h libavutil/opt.h \
libswscale/swscale.h libswresample/swresample.h
headerFilter = libavcodec/** libavformat/** libavutil/** \
libswscale/** libswresample/**
linkerOpts = -lavutil -lavformat -lavcodec -lswscale -lswresample
---
static void av_buffer_unref2(AVBufferRef* ref) {
AVBufferRef* copy = ref;
av_buffer_unref(&copy);
}
static void avcodec_free_context2(AVCodecContext* ref) {
AVCodecContext* copy = ref;
avcodec_free_context(&copy);
}
static void avformat_free_context2(AVFormatContext* ref) {
AVFormatContext* copy = ref;
avformat_free_context(&copy);
}
static void swr_free2(SwrContext* ref) {
SwrContext* copy = ref;
swr_free(&copy);
}
@@ -0,0 +1,7 @@
package = sdl
headers = SDL.h
entryPoint = SDL_main
headerFilter = SDL*
linkerOpts = -lSDL2
@@ -0,0 +1,391 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import ffmpeg.*
import kotlin.native.concurrent.*
import kotlinx.cinterop.*
import platform.posix.memcpy
// This global variable only set to != null value in the decoding worker.
@ThreadLocal
private var decoder: Decoder? = null
data class VideoInfo(val size: Dimensions, val fps: Double)
data class AudioInfo(val sampleRate: Int, val channels: Int)
data class CodecInfo(val video: VideoInfo?, val audio: AudioInfo?) {
val hasVideo = video != null
val hasAudio = audio != null
}
class VideoFrame(val buffer: CPointer<AVBufferRef>, val lineSize: Int, val timeStamp: Double) {
fun unref() = av_buffer_unref2(buffer)
}
class AudioFrame(val buffer: CPointer<AVBufferRef>, var position: Int, val size: Int, val timeStamp: Double) {
fun unref() = av_buffer_unref2(buffer)
}
private fun Int.checkAVError() {
if (this != 0) {
val buffer = ByteArray(1024)
av_strerror(this, buffer.refTo(0), buffer.size.convert())
throw Error("AVError: ${buffer.decodeToString()}")
}
}
private val AVFormatContext.codecs: List<AVCodecContext?>
get() = List(nb_streams.toInt()) { streams?.get(it)?.pointed?.codec?.pointed }
private fun AVFormatContext.streamAt(index: Int): AVStream? =
if (index < 0) null else streams?.get(index)?.pointed
private fun AVStream.openCodec(tag: String): AVCodecContext {
// Get codec context for the video stream.
val codecContext = codec!!.pointed
val codec = avcodec_find_decoder(codecContext.codec_id)?.pointed ?:
throw Error("Unsupported $tag codec with id ${codecContext.codec_id}...")
// Open codec.
if (avcodec_open2(codecContext.ptr, codec.ptr, null) < 0)
throw Error("Couldn't open $tag codec with id ${codecContext.codec_id}")
return codecContext
}
class AVFile(private val fileName: String) : DisposableContainer() {
private val contextPtrPtr = arena.alloc<CPointerVar<AVFormatContext>>().ptr
private val contextPtr: CPointer<AVFormatContext>
init {
avformat_open_input(contextPtrPtr, fileName, null, null).checkAVError()
contextPtr = contextPtrPtr.pointed.value ?: throw Error("Failed to open AV file")
tryConstruct {
if (avformat_find_stream_info(contextPtr, null) < 0)
throw Error("Couldn't find stream information")
}
}
override fun dispose() {
avformat_close_input(contextPtrPtr)
super.dispose()
}
fun dumpFormat() = av_dump_format(contextPtr, 0, fileName, 0)
val context get() = contextPtr.pointed
}
private fun PixelFormat.toAVPixelFormat(): AVPixelFormat? = when (this) {
PixelFormat.RGB24 -> AV_PIX_FMT_RGB24
PixelFormat.ARGB32 -> AV_PIX_FMT_RGB32
PixelFormat.INVALID -> null
}
private data class VideoDecoderOutput(val size: Dimensions, val avPixelFormat: AVPixelFormat)
// Performs data type conversion and copy to transfer data to DecoderWorker
private fun VideoOutput.toVideoDecoderOutput(): VideoDecoderOutput? {
val avPixelFormat = pixelFormat.toAVPixelFormat() ?: return null
return VideoDecoderOutput(size.copy(), avPixelFormat)
}
private class VideoDecoder(
private val videoCodecContext: AVCodecContext,
output: VideoDecoderOutput
) : DisposableContainer() {
private val windowSize = output.size
private val avPixelFormat = output.avPixelFormat
private val videoSize = Dimensions(videoCodecContext.width, videoCodecContext.height)
private val videoFrame: AVFrame =
disposable("av_frame_alloc", ::av_frame_alloc, ::av_frame_unref).pointed
private val scaledVideoFrame: AVFrame =
disposable("av_frame_alloc", ::av_frame_alloc, ::av_frame_unref).pointed
private val softwareScalingContext: CPointer<SwsContext> = disposable(
message = "sws_getContext",
create = {
sws_getContext(
videoSize.w, videoSize.h,
videoCodecContext.pix_fmt,
windowSize.w, windowSize.h, avPixelFormat,
SWS_BILINEAR, null, null, null)
},
dispose = ::sws_freeContext
)
private val scaledFrameSize = avpicture_get_size(avPixelFormat, windowSize.w, windowSize.h)
private val buffer: UByteArray = UByteArray(scaledFrameSize)
private val videoQueue = Queue<VideoFrame>(100)
private val minVideoFrames = 5
init {
avpicture_fill(scaledVideoFrame.ptr.reinterpret(), buffer.refTo(0),
avPixelFormat, windowSize.w, windowSize.h)
}
override fun dispose() {
super.dispose()
while (!videoQueue.isEmpty()) videoQueue.pop().unref()
}
fun isQueueEmpty() = videoQueue.isEmpty()
fun isQueueAlmostFull() = videoQueue.size() > videoQueue.maxSize - 5
fun needMoreFrames() = videoQueue.size() < minVideoFrames
fun nextFrame() = videoQueue.popOrNull()
fun decodeVideoPacket(packet: AVPacket, frameFinished: IntVar) {
// Decode video frame.
avcodec_decode_video2(videoCodecContext.ptr, videoFrame.ptr, frameFinished.ptr, packet.ptr)
// Did we get a video frame?
if (frameFinished.value != 0) {
// Convert the frame from its movie format to window pixel format.
sws_scale(softwareScalingContext, videoFrame.data,
videoFrame.linesize, 0, videoSize.h,
scaledVideoFrame.data, scaledVideoFrame.linesize)
// TODO: reuse buffers!
val buffer = av_buffer_alloc(scaledFrameSize)!!
val ts = av_frame_get_best_effort_timestamp(videoFrame.ptr) *
av_q2d(videoCodecContext.time_base.readValue())
memcpy(buffer.pointed.data, scaledVideoFrame.data[0], scaledFrameSize.convert())
videoQueue.push(VideoFrame(buffer, scaledVideoFrame.linesize[0], ts))
}
}
}
private fun SampleFormat.toAVSampleFormat(): AVSampleFormat? = when (this) {
SampleFormat.S16 -> AV_SAMPLE_FMT_S16
SampleFormat.INVALID -> null
}
private data class AudioDecoderOutput(
val sampleRate: Int,
val channels: Int,
val channelLayout: Int,
val sampleFormat: AVSampleFormat)
// Performs data type conversion and copy to transfer data to DecoderWorker
private fun AudioOutput.toAudioDecoderOutput(): AudioDecoderOutput? {
val avSampleFormat = sampleFormat.toAVSampleFormat() ?: return null
if (channels != 2) return null // only stereo output is supported for now
return AudioDecoderOutput(sampleRate, channels, AV_CH_LAYOUT_STEREO, avSampleFormat)
}
private class AudioDecoder(
private val audioCodecContext: AVCodecContext,
output: AudioDecoderOutput
): DisposableContainer() {
private val audioFrame: AVFrame =
disposable(create = ::av_frame_alloc, dispose = ::av_frame_unref).pointed
private val resampledAudioFrame: AVFrame =
disposable(create = ::av_frame_alloc, dispose = ::av_frame_unref).pointed
private val resampleContext: CPointer<SwrContext> =
disposable(create = ::swr_alloc, dispose = ::swr_free2)
private val audioQueue = Queue<AudioFrame>(100)
private val minAudioFrames = 2
private val maxAudioFrames = 5
init {
with(resampledAudioFrame) {
channels = output.channels
sample_rate = output.sampleRate
format = output.sampleFormat
channel_layout = output.channelLayout.convert()
}
with(audioCodecContext) {
setResampleOpt("in_channel_layout", channel_layout.convert())
setResampleOpt("out_channel_layout", output.channelLayout)
setResampleOpt("in_sample_rate", sample_rate)
setResampleOpt("out_sample_rate", output.sampleRate)
setResampleOpt("in_sample_fmt", sample_fmt)
setResampleOpt("out_sample_fmt", output.sampleFormat)
}
swr_init(resampleContext)
}
private fun setResampleOpt(name: String, value: Int) =
av_opt_set_int(resampleContext, name, value.signExtend(), 0)
override fun dispose() {
super.dispose()
while (!audioQueue.isEmpty()) audioQueue.pop().unref()
}
fun isSynced(): Boolean = audioQueue.size() < maxAudioFrames
fun isQueueEmpty() = audioQueue.isEmpty()
fun isQueueAlmostFull() = audioQueue.size() > audioQueue.maxSize - 20
fun needMoreFrames() = audioQueue.size() < minAudioFrames
fun nextFrame(size: Int): AudioFrame? {
val frame = audioQueue.peek() ?: return null
val realSize = if (frame.position + size > frame.size) frame.size - frame.position else size
return if (frame.position + realSize == frame.size) {
audioQueue.pop()
} else {
val result = AudioFrame(av_buffer_ref(frame.buffer)!!, frame.position, frame.size, frame.timeStamp)
frame.position += realSize
result
}
}
fun decodeAudioPacket(packet: AVPacket, frameFinished: IntVar) {
while (packet.size > 0) {
val size = avcodec_decode_audio4(audioCodecContext.ptr, audioFrame.ptr, frameFinished.ptr, packet.ptr)
if (frameFinished.value != 0) {
// Put audio frame to decoder's queue.
swr_convert_frame(resampleContext, resampledAudioFrame.ptr, audioFrame.ptr).checkAVError()
with(resampledAudioFrame) {
val audioFrameSize = av_samples_get_buffer_size(null, channels, nb_samples, format, 1)
val buffer = av_buffer_alloc(audioFrameSize)!!
val ts = av_frame_get_best_effort_timestamp(audioFrame.ptr) *
av_q2d(audioCodecContext.time_base.readValue())
memcpy(buffer.pointed.data, data[0], audioFrameSize.convert())
audioQueue.push(AudioFrame(buffer, 0, audioFrameSize, ts))
}
}
packet.size -= size
packet.data += size
}
}
}
private class Decoder(
private val formatContext: CPointer<AVFormatContext>,
private val videoStreamIndex: Int,
private val audioStreamIndex: Int,
private val videoCodecContext: AVCodecContext?,
private val audioCodecContext: AVCodecContext?
) {
private var video: VideoDecoder? = null
private var audio: AudioDecoder? = null
var noMoreFrames = false
fun start(videoOutput: VideoDecoderOutput?, audioOutput: AudioDecoderOutput?) {
video = videoCodecContext?.let { ctx -> videoOutput?.let { VideoDecoder(ctx, it) } }
audio = audioCodecContext?.let { ctx -> audioOutput?.let { AudioDecoder(ctx, it) } }
noMoreFrames = false
decodeIfNeeded()
}
fun done() = noMoreFrames && (video?.isQueueEmpty() ?: true) && (audio?.isQueueEmpty() ?: true)
fun dispose() {
video?.dispose()
audio?.dispose()
}
private fun needMoreFrames(): Boolean =
(video?.needMoreFrames() ?: false) || (audio?.needMoreFrames() ?: false)
fun decodeIfNeeded() {
if (!needMoreFrames()) return
if (video?.isQueueAlmostFull() == true) return
if (audio?.isQueueAlmostFull() == true) return
memScoped {
val packet = alloc<AVPacket>()
val frameFinished = alloc<IntVar>()
while (needMoreFrames() && av_read_frame(formatContext, packet.ptr) >= 0) {
when (packet.stream_index) {
videoStreamIndex -> video?.decodeVideoPacket(packet, frameFinished)
audioStreamIndex -> audio?.decodeAudioPacket(packet, frameFinished)
}
av_packet_unref(packet.ptr)
}
if (needMoreFrames()) noMoreFrames = true
}
}
fun nextVideoFrame(): VideoFrame? {
decodeIfNeeded()
return video?.nextFrame()
}
fun nextAudioFrame(size: Int): AudioFrame? {
decodeIfNeeded()
return audio?.nextFrame(size)
}
fun audioVideoSynced() = (audio?.isSynced() ?: true) || done()
}
inline class DecoderWorker(val worker: Worker) : Disposable {
// This class must have no other state, but this worker object.
// All the real state must be stored on the worker's side.
constructor() : this(Worker.start())
override fun dispose() {
worker.requestTermination().result
}
fun initDecode(context: AVFormatContext, useVideo: Boolean = true, useAudio: Boolean = true): CodecInfo {
// Find the first video/audio streams.
val videoStreamIndex =
if (useVideo) context.codecs.indexOfFirst { it?.codec_type == AVMEDIA_TYPE_VIDEO } else -1
val audioStreamIndex =
if (useAudio) context.codecs.indexOfFirst { it?.codec_type == AVMEDIA_TYPE_AUDIO } else -1
val videoStream = context.streamAt(videoStreamIndex)
val audioStream = context.streamAt(audioStreamIndex)
val videoContext = videoStream?.openCodec("video")
val audioContext = audioStream?.openCodec("audio")
// Extract video info.
val video = videoContext?.run {
VideoInfo(Dimensions(width, height), av_q2d(av_stream_get_r_frame_rate(videoStream.ptr)))
}
// Extract audio info.
val audio = audioContext?.run {
AudioInfo(sample_rate, channels)
}
// Pack all state and pass it to the worker.
worker.execute(TransferMode.SAFE, {
Decoder(context.ptr,
videoStreamIndex, audioStreamIndex,
videoContext, audioContext)
}) { decoder = it }
return CodecInfo(video, audio)
}
fun start(videoOutput: VideoOutput, audioOutput: AudioOutput) {
worker.execute(TransferMode.SAFE,
{ Pair(
videoOutput.toVideoDecoderOutput(),
audioOutput.toAudioDecoderOutput())
}) {
decoder?.start(it.first, it.second)
}
}
fun stop() {
worker.execute(TransferMode.SAFE, { null }) {
decoder?.run {
dispose()
decoder = null
}
}.result
}
fun done(): Boolean =
worker.execute(TransferMode.SAFE, { null }) { decoder?.done() ?: true }.result
fun requestDecodeChunk() =
worker.execute(TransferMode.SAFE, { null }) { decoder?.decodeIfNeeded() }.result
fun nextVideoFrame(): VideoFrame? =
worker.execute(TransferMode.SAFE, { null }) { decoder?.nextVideoFrame() }.result
fun nextAudioFrame(size: Int): AudioFrame? =
worker.execute(TransferMode.SAFE, { size }) { decoder?.nextAudioFrame(it) }.result
fun audioVideoSynced(): Boolean =
worker.execute(TransferMode.SAFE, { null }) { decoder?.audioVideoSynced() ?: true }.result
}
@@ -0,0 +1,11 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
data class Dimensions(val w: Int, val h: Int) {
operator fun minus(other: Dimensions) = Dimensions(w - other.w, h - other.h)
operator fun div(b: Int) = Dimensions(w / b, h / b)
}
@@ -0,0 +1,70 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import kotlinx.cinterop.*
/**
* Disposable class manages and owns all its native resources. It allocates some resources
* during construction and may allocate some additional resources during operation.
* It must free all its resource once [dispose] is invoked. "Disposed" is a final state of the disposable
* class. It is not supposed to be used after being disposed.
*/
interface Disposable {
/**
* Disposes all native resources owned by this class. This function must be invoked
* exactly once as the last operation on the corresponding class.
*/
fun dispose()
}
/**
* Helper class to implement [Disposable] interface. It contains an [arena] for native
* memory allocations and a number of helper methods to simplify management of other
* kinds of native resources.
*
* It is important to wrap all potentially exception-throwing code in the class constructor
* into [tryConstruct] invocation, because, when object construction fails with exception,
* it ensures that all the resource that were allocated so far will get freed.
*/
abstract class DisposableContainer : Disposable {
val arena = Arena()
override fun dispose() {
arena.clear()
}
inline fun <T> tryConstruct(init: () -> T): T =
try { init() }
catch (e: Throwable) {
dispose()
throw e
}
inline fun <T> disposable(
message: String = "disposable",
create: () -> T?,
crossinline dispose: (T) -> Unit
): T =
tryConstruct {
create()?.also {
arena.defer { dispose(it) }
} ?: throw Error(message)
}
inline fun <T : Disposable> disposable(create: () -> T): T =
disposable(
create = create,
dispose = { it.dispose() })
inline fun <T : CPointed> sdlDisposable(
message: String,
ptr: CPointer<T>?,
crossinline dispose: (CPointer<T>) -> Unit): CPointer<T> =
disposable(
create = { ptr ?: throwSDLError(message) },
dispose = dispose)
}
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
class Queue<T>(val maxSize: Int) {
private val array = arrayOfNulls<Any>(maxSize)
private var head = 0
private var tail = 0
fun push(element: T) {
if ((tail + 1) % maxSize == head)
throw Error("queue overflow: $tail $head")
array[tail] = element
tail = (tail + 1) % maxSize
}
@Suppress("UNCHECKED_CAST")
fun pop(): T {
if (tail == head)
throw Error("queue underflow")
val result = array[head] as T
array[head] = null
head = (head + 1) % maxSize
return result
}
@Suppress("UNCHECKED_CAST")
fun peek() : T? {
if (isEmpty()) return null
return array[head] as T
}
fun size() = if (tail >= head) tail - head else maxSize - (head - tail)
fun isEmpty() = head == tail
fun popOrNull(): T? = if (isEmpty()) null else pop()
}
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import kotlin.native.concurrent.Worker
import kotlinx.cinterop.*
import sdl.*
import platform.posix.memset
import platform.posix.memcpy
enum class SampleFormat {
INVALID,
S16
}
data class AudioOutput(val sampleRate: Int, val channels: Int, val sampleFormat: SampleFormat)
private fun SampleFormat.toSDLFormat(): SDL_AudioFormat? = when (this) {
SampleFormat.S16 -> AUDIO_S16SYS.convert()
SampleFormat.INVALID -> null
}
class SDLAudio(private val player: VideoPlayer) : DisposableContainer() {
private var state = State.STOPPED
fun start(audio: AudioOutput) {
stop()
val audioFormat = audio.sampleFormat.toSDLFormat() ?: return
println("SDL Audio: Playing output with ${audio.channels} channels, ${audio.sampleRate} samples per second")
memScoped {
// TODO: better mechanisms to ensure we have same output format here and in resampler of the decoder.
val spec = alloc<SDL_AudioSpec>().apply {
freq = audio.sampleRate
format = audioFormat
channels = audio.channels.convert()
silence = 0u
samples = 4096u
userdata = player.worker.asCPointer()
callback = staticCFunction(::audioCallback)
}
val realSpec = alloc<SDL_AudioSpec>()
if (SDL_OpenAudio(spec.ptr, realSpec.ptr) < 0)
throwSDLError("SDL_OpenAudio")
// TODO: ensure real spec matches what we asked for.
state = State.PAUSED
resume()
}
}
fun pause() {
state = state.transition(State.PLAYING, State.PAUSED) { SDL_PauseAudio(1) }
}
fun resume() {
state = state.transition(State.PAUSED, State.PLAYING) { SDL_PauseAudio(0) }
}
fun stop() {
pause()
state = state.transition(State.PAUSED, State.STOPPED) { SDL_CloseAudio() }
}
}
private fun audioCallback(userdata: COpaquePointer?, buffer: CPointer<Uint8Var>?, length: Int) {
// This handler will be invoked in the audio thread, so reinit runtime.
initRuntimeIfNeeded()
val decoder = DecoderWorker(Worker.fromCPointer(userdata))
var outPosition = 0
while (outPosition < length) {
val frame = decoder.nextAudioFrame(length - outPosition)
if (frame != null) {
val toCopy = minOf(length - outPosition, frame.size - frame.position)
memcpy(buffer + outPosition, frame.buffer.pointed.data + frame.position, toCopy.convert())
frame.unref()
outPosition += toCopy
} else {
// println("Decoder returned nothing!")
memset(buffer + outPosition, 0, (length - outPosition).convert())
break
}
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import sdl.SDL_GetError
import kotlinx.cinterop.*
fun throwSDLError(name: String): Nothing =
throw Error("SDL_$name Error: ${SDL_GetError()!!.toKString()}")
fun checkSDLError(name: String, result: Int) {
if (result != 0) throwSDLError(name)
}
@@ -0,0 +1,28 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import kotlinx.cinterop.*
import sdl.*
class SDLInput(private val player: VideoPlayer) : DisposableContainer() {
private val event = arena.alloc<SDL_Event>().ptr
fun check() {
while (SDL_PollEvent(event.reinterpret()) != 0) {
when (event.pointed.type) {
SDL_QUIT -> player.stop()
SDL_KEYDOWN -> {
val keyboardEvent = event.reinterpret<SDL_KeyboardEvent>().pointed
when (keyboardEvent.keysym.scancode) {
SDL_SCANCODE_ESCAPE -> player.stop()
SDL_SCANCODE_SPACE -> player.pause()
}
}
}
}
}
}
@@ -0,0 +1,99 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import kotlinx.cinterop.*
import sdl.*
enum class PixelFormat {
INVALID,
RGB24,
ARGB32
}
data class VideoOutput(val size: Dimensions, val pixelFormat: PixelFormat)
class SDLVideo : DisposableContainer() {
private val displaySize: Dimensions
private var window: SDLRendererWindow? = null
init {
disposable(
create = { checkSDLError("Init", SDL_Init(SDL_INIT_EVERYTHING)) },
dispose = { SDL_Quit() })
displaySize = tryConstruct {
memScoped {
alloc<SDL_DisplayMode>().run {
checkSDLError("GetCurrentDisplayMode", SDL_GetCurrentDisplayMode(0, ptr.reinterpret()))
Dimensions(w, h)
}
}
}
}
override fun dispose() {
stop()
super.dispose()
}
fun start(videoSize: Dimensions) {
stop() // To free resources from previous playbacks.
println("SDL Video: Playing output with ${videoSize.w} x ${videoSize.h} pixels")
window = SDLRendererWindow((displaySize - videoSize) / 2, videoSize)
}
fun pixelFormat(): PixelFormat = window?.pixelFormat() ?: PixelFormat.INVALID
fun nextFrame(frameData: CPointer<Uint8Var>, linesize: Int) =
window?.nextFrame(frameData, linesize)
fun stop() {
window?.let {
it.dispose()
window = null
}
}
}
class SDLRendererWindow(windowPos: Dimensions, videoSize: Dimensions) : DisposableContainer() {
private val window = sdlDisposable("CreateWindow",
SDL_CreateWindow("VideoPlayer", windowPos.w, windowPos.h, videoSize.w, videoSize.h, SDL_WINDOW_SHOWN),
::SDL_DestroyWindow)
private val renderer = sdlDisposable("CreateRenderer",
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC),
::SDL_DestroyRenderer)
private val texture = sdlDisposable("CreateTexture",
SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), 0, videoSize.w, videoSize.h),
::SDL_DestroyTexture)
private val rect = sdlDisposable("calloc(SDL_Rect)",
SDL_calloc(1, sizeOf<SDL_Rect>().convert()), ::SDL_free)
.reinterpret<SDL_Rect>()
init {
rect.pointed.apply {
x = 0
y = 0
w = videoSize.w
h = videoSize.h
}
}
fun pixelFormat(): PixelFormat = when (SDL_GetWindowPixelFormat(window)) {
SDL_PIXELFORMAT_RGB24 -> PixelFormat.RGB24
SDL_PIXELFORMAT_ARGB8888, SDL_PIXELFORMAT_RGB888 -> PixelFormat.ARGB32
else -> {
println("Pixel format ${SDL_GetWindowPixelFormat(window)} unknown")
PixelFormat.INVALID
}
}
fun nextFrame(frameData: CPointer<Uint8Var>, linesize: Int) {
SDL_UpdateTexture(texture, rect, frameData, linesize)
SDL_RenderClear(renderer)
SDL_RenderCopy(renderer, texture, rect, rect)
SDL_RenderPresent(renderer)
}
}
@@ -0,0 +1,181 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package sample.videoplayer
import ffmpeg.*
import kotlinx.cinterop.*
import platform.posix.*
import kotlinx.cli.*
enum class State {
PLAYING,
STOPPED,
PAUSED;
inline fun transition(from: State, to: State, block: () -> Unit): State =
if (this == from) {
block()
to
} else this
}
enum class PlayMode {
VIDEO,
AUDIO,
BOTH;
val useVideo: Boolean get() = this != AUDIO
val useAudio: Boolean get() = this != VIDEO
}
class VideoPlayer(private val requestedSize: Dimensions?) : DisposableContainer() {
private val decoder = disposable { DecoderWorker() }
private val video = disposable { SDLVideo() }
private val audio = disposable { SDLAudio(this) }
private val input = disposable { SDLInput(this) }
private val now = arena.alloc<timespec>().ptr
private var state = State.STOPPED
val worker get() = decoder.worker
var lastFrameTime = 0.0
fun stop() {
state = State.STOPPED
}
fun pause() {
when (state) {
State.PAUSED -> {
state = State.PLAYING
audio.resume()
}
State.PLAYING -> {
state = State.PAUSED
audio.pause()
}
State.STOPPED -> throw Error("Cannot pause in stopped state")
}
}
private fun getTime(): Double {
clock_gettime(CLOCK_MONOTONIC, now)
return now.pointed.tv_sec + now.pointed.tv_nsec / 1e9
}
fun playFile(fileName: String, mode: PlayMode) {
println("playFile $fileName")
val file = AVFile(fileName)
try {
file.dumpFormat()
val info = decoder.initDecode(file.context, mode.useVideo, mode.useAudio)
val videoSize = requestedSize ?: info.video?.size ?: Dimensions(400, 200)
// Use requested video size to start SDLVideo
info.video?.let { video.start(videoSize) }
// Configure decoder output based on actual SDLVideo pixel format
val videoOutput = VideoOutput(videoSize, video.pixelFormat())
// Use fixed audio output format
val audioOutput = AudioOutput(44100, 2, SampleFormat.S16)
// Start decoder
decoder.start(videoOutput, audioOutput)
// Start SDLAudio player
info.audio?.let { audio.start(audioOutput) }
// Main player loop
lastFrameTime = getTime()
state = State.PLAYING
decoder.requestDecodeChunk() // Fill in frame caches
while (state != State.STOPPED) {
// Fetch video
info.video?.let { playVideoFrame(it) }
// Audio is being auto-fetched by the audio thread
// Check if there are any input
input.check()
// Pause support
checkPause()
// Inter-frame pause, may lead to broken A/V sync, think of better approach
if (state == State.PLAYING) syncAV(info)
if (decoder.done()) stop()
}
} finally {
stop()
audio.stop()
video.stop()
decoder.stop()
file.dispose()
}
}
private fun playVideoFrame(videoInfo: VideoInfo) {
// Fetch next frame
val frame = decoder.nextVideoFrame() ?: return
// Use video FPS to maintain frame rate
val now = getTime()
val frameDuration = 1.0 / videoInfo.fps
val passedTime = now - lastFrameTime
lastFrameTime += frameDuration // try to maintain perfect frame rate
// Wait for next frame, if needed
if (passedTime < frameDuration) {
usleep((1000_000 * (frameDuration - passedTime)).toInt().toUInt())
} else if (passedTime > frameDuration * 1.5){
lastFrameTime = now // we fell behind more than half frame, reset time
}
// Play frame
video.nextFrame(frame.buffer.pointed.data!!, frame.lineSize)
frame.unref()
}
private fun checkPause() {
while (state == State.PAUSED) {
audio.pause()
input.check()
usleep(1u * 1000u)
}
audio.resume()
}
private fun syncAV(info: CodecInfo) {
if (info.hasVideo) {
if (info.hasAudio) {
// Use sound for A/V sync.
if (!decoder.audioVideoSynced()) {
println("Resynchronizing video with audio")
while (!decoder.audioVideoSynced() && state == State.PLAYING) {
usleep(500)
input.check()
}
}
}
} else {
// For pure sound, playback is driven by demand.
usleep(10u * 1000u)
}
}
}
fun main(args: Array<String>) {
val argParser = ArgParser("videoplayer")
val mode by argParser.option(
ArgType.Choice<PlayMode>(), shortName = "m", description = "Play mode")
.default(PlayMode.BOTH)
val size by argParser.option(ArgType.Int, shortName = "s", description = "Required size of videoplayer window")
.delimiter(",")
val fileName by argParser.argument(ArgType.String, description = "File to play")
argParser.parse(args)
av_register_all()
val requestedSize = if (size.size != 2) {
if (size.isNotEmpty())
println("Size value should include width and height separated with ','.")
null
} else
Dimensions(size[0], size[1])
val player = VideoPlayer(requestedSize)
try {
player.playFile(fileName, mode)
} finally {
player.dispose()
}
}