Videoplayer example (#1031)

This commit is contained in:
Nikolay Igotti
2017-11-14 19:39:52 +03:00
committed by GitHub
parent 9d7ed9d153
commit 131c99d535
13 changed files with 1033 additions and 0 deletions
@@ -101,6 +101,10 @@ class Future<T> internal constructor(val id: FutureId) {
throw IllegalStateException("Future is cancelled")
}
fun result(): T {
return consume { it -> it }
}
val state: FutureState
get() = FutureState.values()[stateOfFuture(id)]
+1
View File
@@ -14,4 +14,5 @@ include ':tensorflow'
include ':objc'
include ':uikit'
include ':win32'
include ':videoplayer'
includeBuild '../'
+16
View File
@@ -0,0 +1,16 @@
# Simple video player
This example shows how one could implement a video player in Kotlin.
Almost any video file supported by ffmpeg could be played with it.
ffmpeg and SDL2 is needed for that to work, i.e.
port install ffmpeg-devel
apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
libavresample-dev
apt install libsdl2-dev
To build use `../gradlew build`.
To run use `./build/konan/bin/<platform>/Player.kexe file.mp4`.
+45
View File
@@ -0,0 +1,45 @@
apply plugin: 'konan'
konan.targets = ['macbook', 'linux']
konanArtifacts {
interop('ffmpeg') {
defFile 'src/main/c_interop/ffmpeg.def'
target 'linux', {
includeDirs '/usr/include', '/usr/include/x86_64-linux-gnu'
}
target 'macbook', {
includeDirs '/opt/local/include', '/usr/local/include'
}
}
interop ('sdl') {
defFile 'src/main/c_interop/sdl.def'
target 'macbook', {
includeDirs '/Library/Frameworks/SDL2.framework/Headers',
"${System.getProperty("user.home")}/Library/Frameworks/SDL2.framework/Headers",
'/opt/local/include/SDL2',
'/usr/local/include/SDL2'
}
target 'linux', {
includeDirs '/usr/include/SDL2'
}
}
program('Player') {
libraries {
artifact 'ffmpeg'
artifact 'sdl'
}
target 'macbook', {
linkerOpts "-F ${System.getProperty("user.home")}/Library/Frameworks -F /Library/Frameworks -L/opt/local/lib"
}
target 'linux', {
linkerOpts '-L/usr/lib/x86_64-linux-gnu'
}
}
}
@@ -0,0 +1,27 @@
package = ffmpeg
headers = libavcodec/avcodec.h libavformat/avformat.h libavutil/pixfmt.h libavutil/opt.h \
libswscale/swscale.h libavresample/avresample.h
headerFilter = libavcodec/** libavformat/** libavutil/** \
libswscale/** libavresample/**
linkerOpts = -lavutil -lavformat -lavcodec -lswscale -lavresample
---
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 avresample_free2(AVAudioResampleContext* ref) {
AVAudioResampleContext* copy = ref;
avresample_free(&copy);
}
@@ -0,0 +1,8 @@
package = sdl
headers = SDL.h
entryPoint = SDL_main
headerFilter = SDL*
linkerOpts.osx = -lSDL2
linkerOpts.linux = -lSDL2
@@ -0,0 +1,394 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ffmpeg.*
import kotlin.system.*
import kotlinx.cinterop.*
import konan.worker.*
import platform.posix.memcpy
// This global variable only set to != null value in the decoding worker.
private var state: DecodeWorkerState? = null
data class OutputInfo(val width: Int, val height: Int, val pixelFormat: AVPixelFormat)
data class VideoInfo(val width: Int, val height: Int, val fps: Double,
val sampleRate: Int, val channels: Int)
private data class AskNextAudioFrame(val size: Int)
data class VideoFrame(val buffer: CPointer<AVBufferRef>, val lineSize: Int) {
fun unref() {
av_buffer_unref2(buffer)
}
}
data class AudioFrame(val buffer: CPointer<AVBufferRef>, var position: Int, val size: Int) {
fun unref() {
av_buffer_unref2(buffer)
}
}
private fun Int.checkError() {
if (this != 0) {
val buffer = ByteArray(1024)
av_strerror(this, buffer.refTo(0), buffer.size.signExtend())
throw Error("AVError: ${kotlin.text.fromUtf8Array(buffer, 0, buffer.size)}")
}
}
class DecodeWorkerState(val formatContext: CPointer<AVFormatContext>,
val videoStreamIndex: Int,
val audioStreamIndex: Int,
val videoCodecContext: CPointer<AVCodecContext>?,
val audioCodecContext: CPointer<AVCodecContext>?) {
var videoFrame: CPointer<AVFrame>? = null
var scaledVideoFrame: CPointer<AVFrame>? = null
var audioFrame: CPointer<AVFrame>? = null
var resampledAudioFrame: CPointer<AVFrame>? = null
var softwareScalingContext: CPointer<SwsContext>? = null
var resampleContext: CPointer<AVAudioResampleContext>? = null
val videoQueue = Queue<VideoFrame?>(100, null)
val audioQueue = Queue<AudioFrame?>(100, null)
var buffer: ByteArray? = null
var videoWidth = 0
var videoHeight = 0
var windowWidth = 0
var windowHeight = 0
var scaledFrameSize = 0
var noMoreFrames = false
val minAudioFrames = 1
val maxAudioFrames = 3
val minVideoFrames = 5
fun makeVideoFrame(): VideoFrame {
// TODO: reuse buffers!
val buffer = av_buffer_alloc(scaledFrameSize)!!
memcpy(buffer.pointed.data, scaledVideoFrame!!.pointed.data[0], scaledFrameSize.signExtend())
return VideoFrame(buffer, scaledVideoFrame!!.pointed.linesize[0])
}
fun makeAudioFrame(): AudioFrame {
avresample_convert_frame(resampleContext, resampledAudioFrame, audioFrame).checkError()
val audioFrameSize = av_samples_get_buffer_size(
null,
resampledAudioFrame!!.pointed.channels,
resampledAudioFrame!!.pointed.nb_samples,
resampledAudioFrame!!.pointed.format,
1)
val buffer = av_buffer_alloc(audioFrameSize)!!
memcpy(buffer.pointed.data, resampledAudioFrame!!.pointed.data[0], audioFrameSize.signExtend())
return AudioFrame(buffer, 0, audioFrameSize)
}
private fun setResampleOpt(name: String, value: Int) =
av_opt_set_int(resampleContext, name, value.signExtend(), 0)
fun start(output: OutputInfo) {
if (videoCodecContext != null) {
videoWidth = videoCodecContext.pointed.width
videoHeight = videoCodecContext.pointed.height
windowWidth = output.width
windowHeight = output.height
videoFrame = av_frame_alloc()!!
scaledVideoFrame = av_frame_alloc()!!
softwareScalingContext = sws_getContext(
videoWidth,
videoHeight,
videoCodecContext.pointed.pix_fmt,
windowWidth, windowHeight, output.pixelFormat,
SWS_BILINEAR, null, null, null)!!
scaledFrameSize = avpicture_get_size(output.pixelFormat, output.width, output.height)
buffer = ByteArray(scaledFrameSize)
avpicture_fill(scaledVideoFrame!!.reinterpret(), buffer!!.refTo(0),
output.pixelFormat, output.width, output.height)
}
if (audioCodecContext != null) {
audioFrame = av_frame_alloc()!!
resampledAudioFrame = av_frame_alloc()!!
resampledAudioFrame!!.pointed.format = AV_SAMPLE_FMT_S16
resampledAudioFrame!!.pointed.channels = 2
resampledAudioFrame!!.pointed.channel_layout = AV_CH_LAYOUT_STEREO.signExtend()
resampledAudioFrame!!.pointed.sample_rate = 44100
resampleContext = avresample_alloc_context()
setResampleOpt("in_channel_layout", audioCodecContext.pointed.channel_layout.narrow())
setResampleOpt("out_channel_layout", AV_CH_LAYOUT_STEREO)
setResampleOpt("in_sample_rate", audioCodecContext.pointed.sample_rate)
setResampleOpt("out_sample_rate", 44100)
setResampleOpt("in_sample_fmt", audioCodecContext.pointed.sample_fmt)
setResampleOpt("out_sample_fmt", AV_SAMPLE_FMT_S16)
avresample_open(resampleContext)
}
noMoreFrames = false
decodeIfNeeded()
}
fun done() = noMoreFrames && videoQueue.isEmpty() && audioQueue.isEmpty()
fun stop() {
while (!videoQueue.isEmpty()) {
videoQueue.pop()?.unref()
}
while (!audioQueue.isEmpty()) {
audioQueue.pop()?.unref()
}
if (videoFrame != null) {
av_frame_unref(videoFrame)
videoFrame = null
}
if (scaledVideoFrame != null) {
av_frame_unref(scaledVideoFrame)
scaledVideoFrame = null
}
if (audioFrame != null) {
av_frame_unref(audioFrame)
audioFrame = null
}
if (resampledAudioFrame != null) {
av_frame_unref(resampledAudioFrame)
resampledAudioFrame = null
}
if (softwareScalingContext != null) {
sws_freeContext(softwareScalingContext)
softwareScalingContext = null
}
if (resampleContext != null) {
avresample_free2(resampleContext)
resampleContext = null
}
if (videoCodecContext != null) {
avcodec_free_context2(videoCodecContext)
}
if (audioCodecContext != null) {
avcodec_free_context2(audioCodecContext)
}
//avformat_free_context2(formatContext)
}
fun needMoreBuffers(): Boolean {
return ((videoStreamIndex != -1) && (videoQueue.size() < minVideoFrames)) ||
((audioStreamIndex != -1) && (audioQueue.size() < minAudioFrames))
}
fun decodeIfNeeded() {
if (!needMoreBuffers()) return
memScoped {
val packet = alloc<AVPacket>()
val frameFinished = alloc<IntVar>()
while (needMoreBuffers() && av_read_frame(formatContext, packet.ptr) >= 0) {
when (packet.stream_index) {
videoStreamIndex -> {
// Decode video frame.
avcodec_decode_video2(videoCodecContext, videoFrame, 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!!.pointed.data,
videoFrame!!.pointed.linesize, 0, videoHeight,
scaledVideoFrame!!.pointed.data, scaledVideoFrame!!.pointed.linesize)
videoQueue.push(makeVideoFrame())
}
}
audioStreamIndex -> {
while (packet.size > 0) {
val size = avcodec_decode_audio4(
audioCodecContext, audioFrame, frameFinished.ptr, packet.ptr)
if (frameFinished.value != 0) {
// Put audio frame to decoder's queue.
audioQueue.push(makeAudioFrame())
}
packet.size -= size
packet.data += size
}
}
}
av_packet_unref(packet.ptr)
}
if (needMoreBuffers()) noMoreFrames = true
}
}
fun nextVideoFrame(): VideoFrame? {
decodeIfNeeded()
if (videoQueue.isEmpty()) {
return null
}
return videoQueue.pop()
}
fun nextAudioFrame(size: Int): AudioFrame? {
decodeIfNeeded()
if (audioQueue.isEmpty()) {
return null
}
val frame = audioQueue.peek()!!
var realSize = if (frame.position + size > frame.size) frame.size - frame.position else size
if (frame.position + realSize == frame.size) {
return audioQueue.pop()
} else {
val result = AudioFrame(av_buffer_ref(frame.buffer)!!, frame.position, frame.size)
frame.position += realSize
return result
}
}
fun audioVideoSynced() = (audioQueue.size() < maxAudioFrames) || done()
}
class DecodeWorker {
// This class must have no other state, but this worker object.
// All the real state must be stored on the worker's side.
private val decodeWorker: Worker
constructor() {
decodeWorker = konan.worker.startWorker()
}
constructor(id: WorkerId) {
decodeWorker = Worker(id)
}
fun workerId() = decodeWorker.id
fun init() {}
fun renderPixelFormat(pixelFormat: PixelFormat) = when (pixelFormat) {
PixelFormat.RGB24 -> AV_PIX_FMT_RGB24
PixelFormat.ARGB32 -> AV_PIX_FMT_RGB32
PixelFormat.INVALID -> AV_PIX_FMT_NONE
}
private fun findStream(useStream: Boolean, formatContext: CPointer<AVFormatContext>, streamIndex: Int, tag: String):
Pair<CPointer<AVStream>?, CPointer<AVCodecContext>?> {
if (streamIndex < 0 || !useStream) return null to null
val stream = formatContext.pointed.streams!!.get(streamIndex)!!
// Get codec context for the video stream.
val codecContext = stream.pointed.codec!!
val codec = avcodec_find_decoder(codecContext.pointed.codec_id)
if (codec == null)
throw Error("Unsupported $tag codec...")
// Open codec.
if (avcodec_open2(codecContext, codec, null) < 0)
throw Error("Couldn't open $tag codec")
return stream to codecContext
}
fun initDecode(file: String, useVideo: Boolean = true, useAudio: Boolean = true): VideoInfo {
memScoped {
try {
val formatContextPtr = alloc<CPointerVar<AVFormatContext>>()
if (avformat_open_input(formatContextPtr.ptr, file, null, null) != 0)
throw Error("Cannot open video file")
val formatContext = formatContextPtr.value!!
if (avformat_find_stream_info(formatContext, null) < 0)
throw Error("Couldn't find stream information")
av_dump_format(formatContext, 0, file, 0)
// Find the first video/audio streams.
var videoStreamIndex = -1
var audioStreamIndex = -1
for (i in 0 until formatContext.pointed.nb_streams) {
val stream = formatContext.pointed.streams!!.get(i)
val codec = stream!!.pointed.codec!!.pointed
if (codec.codec_type == AVMEDIA_TYPE_VIDEO && videoStreamIndex == -1) {
videoStreamIndex = i
}
if (codec.codec_type == AVMEDIA_TYPE_AUDIO && audioStreamIndex == -1) {
audioStreamIndex = i
}
}
val (videoStream, videoCodecContext) = findStream(useVideo, formatContext, videoStreamIndex, "video")
val (_, audioCodecContext) = findStream(useAudio, formatContext, audioStreamIndex, "audio")
// Extract video info.
val (videoWidth, videoHeight, fps) = if (videoCodecContext != null) {
Triple(videoCodecContext.pointed.width, videoCodecContext.pointed.height,
av_q2d(av_stream_get_r_frame_rate(videoStream)))
} else {
Triple(-1, -1, 0.0)
}
val (sampleRate, channels) = if (audioCodecContext != null) {
Pair(audioCodecContext.pointed.sample_rate, audioCodecContext.pointed.channels)
} else {
Pair(0, 0)
}
// Pack all inited state and pass it to the worker.
decodeWorker.schedule(TransferMode.CHECKED, {
DecodeWorkerState(formatContext,
videoStreamIndex, audioStreamIndex,
videoCodecContext, audioCodecContext)
}) { input ->
state = input
null
}
return VideoInfo(videoWidth, videoHeight, fps, sampleRate, channels)
} finally {
// TODO: clean up whatever we allocated.
}
}
}
fun start(width: Int, height: Int, pixelFormat: PixelFormat) {
decodeWorker.schedule(TransferMode.CHECKED, {
OutputInfo(width, height, renderPixelFormat(pixelFormat))
}) { input -> state!!.start(input) }
}
fun stop() {
decodeWorker.schedule(
TransferMode.CHECKED,
{ null }) { _ ->
state!!.stop()
state = null
}.result()
}
// TODO: we manually box returned primitive value,
// fix by autoboxing schedule()'s result in the compiler.
fun done() = decodeWorker.schedule(TransferMode.CHECKED,
{ null }) { _ -> (state == null || state!!.done()) as Boolean?
}.consume { it -> it!! }
fun deinit() {
decodeWorker.requestTermination().result()
}
fun requestDecodeChunk() = decodeWorker.schedule(
TransferMode.CHECKED,
{ null }) { _ -> state!!.decodeIfNeeded() }
fun nextVideoFrame(): VideoFrame? = decodeWorker.schedule(
TransferMode.CHECKED,
{ null }) { _ -> state!!.nextVideoFrame() }.result()
fun nextAudioFrame(size: Int) = decodeWorker.schedule(
TransferMode.CHECKED,
{ size as Int? }) { input -> state!!.nextAudioFrame(input!!) }.result()
fun audioVideoSynced() = decodeWorker.schedule(
TransferMode.CHECKED,
{ null }) { _ -> state!!.audioVideoSynced() as Boolean? }.consume { it -> it!! }
}
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class Queue<T>(val maxSize: Int, val none: T) {
val array = Array<T>(maxSize, { _ -> none})
var head = 0
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
}
fun pop() : T {
if (tail == head)
throw Error("queue underflow")
val result = array[head]
array[head] = none
head = (head + 1) % maxSize
return result
}
fun peek() : T {
if (tail == head)
throw Error("queue underflow")
return array[head]
}
fun size() = if (tail >= head) tail - head else maxSize - (head - tail)
fun isEmpty() = head == tail
}
@@ -0,0 +1,100 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import kotlinx.cinterop.*
import sdl.*
import konan.worker.WorkerId
import platform.posix.memset
import platform.posix.memcpy
class SDLAudio(val player: VideoPlayer) : SDLBase() {
private var threadData: CPointer<IntVar>? = null
override fun init() {
threadData = nativeHeap.alloc<IntVar>().ptr
}
override fun deinit() {
if (threadData != null) {
nativeHeap.free(threadData!!)
threadData = null
}
}
fun start(sampleRate: Int, channels: Int) {
println("Audio: $channels channels, $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>()
spec.freq = 44100
spec.format = AUDIO_S16SYS.narrow()
spec.channels = 2.narrow()
spec.silence = 0
spec.samples = 4096
spec.callback = staticCFunction {
userdata, buffer, length ->
// This handler will be invoked in the audio thread, so reinit runtime.
konan.initRuntimeIfNeeded()
if (decoder == null) {
val callbackData = userdata!!.reinterpret<IntVar>()
decoder = DecodeWorker(callbackData.pointed.value)
}
var outPosition = 0
while (outPosition < length) {
val frame = decoder!!.nextAudioFrame(length - outPosition)
if (frame != null) {
val toCopy = min(length - outPosition, frame.size - frame.position)
memcpy(buffer + outPosition, frame.buffer.pointed.data + frame.position, toCopy.signExtend())
frame.unref()
outPosition += toCopy
} else {
println("Decoder returned nothing!")
memset(buffer + outPosition, 0, (length - outPosition).signExtend())
break
}
}
}
threadData!!.pointed.value = player.decoder.workerId()
spec.userdata = threadData
val realSpec = alloc<SDL_AudioSpec>()
if (SDL_OpenAudio(spec.ptr, realSpec.ptr) < 0)
throw Error("SDL_OpenAudio: ${get_SDL_Error()}")
// TODO: ensure real spec matches what we asked for.
resume()
}
}
fun pause() {
SDL_PauseAudio(1)
}
fun resume() {
SDL_PauseAudio(0)
}
fun stop() {
pause()
SDL_CloseAudio()
}
}
// This global is only set in the audio thread.
var decoder: DecodeWorker? = null
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import sdl.SDL_GetError
import kotlinx.cinterop.*
open class SDLBase {
protected fun get_SDL_Error() = SDL_GetError()!!.toKString()
open fun init() {}
open fun deinit() {}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import kotlinx.cinterop.*
import sdl.*
class SDLInput(val player: VideoPlayer) : SDLBase() {
var event: CPointer<SDL_Event>? = null
override fun init() {
event = nativeHeap.alloc<SDL_Event>().ptr
}
override fun deinit() {
if (event != null) {
nativeHeap.free(event!!)
event = null
}
}
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,130 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import kotlinx.cinterop.*
import sdl.*
class SDLVideo(val player: VideoPlayer) : SDLBase() {
var displayWidth = 0
var displayHeight = 0
var windowWidth = 0
var windowHeight = 0
private var window: CPointer<SDL_Window>? = null
private var renderer: CPointer<SDL_Renderer>? = null
private var surface: CPointer<SDL_Surface>? = null
private var texture: CPointer<SDL_Texture>? = null
private var rect: CPointer<SDL_Rect>? = null
override fun init() {
if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
println("SDL_Init Error: ${get_SDL_Error()}")
throw Error()
}
memScoped {
val displayMode = alloc<SDL_DisplayMode>()
if (SDL_GetCurrentDisplayMode(0, displayMode.ptr.reinterpret()) != 0) {
println("SDL_GetCurrentDisplayMode Error: ${get_SDL_Error()}")
SDL_Quit()
throw Error()
}
displayWidth = displayMode.w
displayHeight = displayMode.h
}
rect = SDL_calloc(1, SDL_Rect.size)!!.reinterpret<SDL_Rect>()
}
override fun deinit() {
stop()
if (rect != null) {
SDL_free(rect)
rect = null
}
SDL_Quit()
}
fun start(videoWidth: Int, videoHeight: Int) {
// To free resources from previous playbacks.
stop()
windowWidth = videoWidth
windowHeight = videoHeight
rect!!.pointed.let {
it.x = 0
it.y = 0
it.w = windowWidth
it.h = windowHeight
}
val windowX = (displayWidth - windowWidth) / 2
val windowY = (displayHeight - windowHeight) / 2
val window = SDL_CreateWindow("KoPlayer", windowX, windowY, windowWidth, windowHeight, SDL_WINDOW_SHOWN)
if (window == null) {
println("SDL_CreateWindow Error: ${get_SDL_Error()}")
SDL_Quit()
throw Error()
}
this.window = window
val renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED or SDL_RENDERER_PRESENTVSYNC)
if (renderer == null) {
SDL_DestroyWindow(window)
println("SDL_CreateRenderer Error: ${get_SDL_Error()}")
SDL_Quit()
throw Error()
}
this.renderer = renderer
this.texture = SDL_CreateTexture(renderer, SDL_GetWindowPixelFormat(window), 0, videoWidth, videoHeight)
}
fun pixelFormat(): PixelFormat {
if (window == null)
return PixelFormat.INVALID
return 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")
TODO()
}
}
}
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)
}
fun stop() {
if (renderer != null) {
SDL_DestroyRenderer(renderer)
renderer = null
}
if (window != null) {
SDL_DestroyWindow(window)
window = null
}
}
}
@@ -0,0 +1,188 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ffmpeg.*
import kotlin.system.*
import kotlinx.cinterop.*
import platform.posix.*
enum class State {
PLAYING,
STOPPED,
PAUSED
}
enum class PixelFormat {
INVALID,
RGB24,
ARGB32
}
class VideoPlayer(val requestedWidth: Int, val requestedHeight: Int) {
val video = SDLVideo(this)
val audio = SDLAudio(this)
val input = SDLInput(this)
var decoder = DecodeWorker()
var state: State = State.STOPPED
var hasAudio = false
var hasVideo = false
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 var now: CPointer<platform.posix.timespec>? = null
private fun getTime(): Double {
clock_gettime(platform.posix.CLOCK_MONOTONIC, now)
return now!!.pointed.tv_sec + now!!.pointed.tv_nsec / 1_000_000_000.0
}
fun init() {
// Prealloc this one, to avoid frequent allocation.
if (now == null)
now = nativeHeap.alloc<platform.posix.timespec>().ptr
}
fun deinit() {
if (now != null) {
nativeHeap.free(now!!)
now = null
}
}
fun playFile(file: String) {
println("playFile $file")
this.init()
decoder.init()
video.init()
audio.init()
input.init()
try {
val info = decoder.initDecode(file, true, true)
val windowWidth = if (requestedWidth == 0) {
if (info.width < 0) 400 else info.width
} else requestedWidth
val windowHeight = if (requestedHeight == 0) {
if (info.height < 0) 200 else info.height
} else requestedHeight
hasVideo = info.width > 0 && info.height > 0
hasAudio = info.sampleRate > 0 && info.channels > 0
if (hasVideo)
video.start(windowWidth, windowHeight)
decoder.start(windowWidth, windowHeight, video.pixelFormat())
if (hasAudio)
audio.start(info.sampleRate, info.channels)
var lastTimeStamp = getTime()
state = State.PLAYING
// Fill in frame caches.
decoder.requestDecodeChunk().result()
while (state != State.STOPPED) {
if (hasVideo) {
val frame = decoder.nextVideoFrame()
if (frame == null) {
state = State.STOPPED
continue
}
video.nextFrame(frame.buffer.pointed.data!!, frame.lineSize)
frame.unref()
}
// Audio is being auto-fetched by the audio thread.
// Check if there are any input.
input.check()
// Pause support.
while (state == State.PAUSED) {
if (hasAudio) audio.pause()
input.check()
usleep(1 * 1000)
}
if (hasAudio) audio.resume()
// Interframe pause, may lead to broken A/V sync, think of better approach.
if (state == State.PLAYING) {
if (hasVideo) {
if (hasAudio) {
// Use sound for A/V sync.
while (!decoder.audioVideoSynced() && state == State.PLAYING) {
usleep(500)
input.check()
}
} else {
// Use video FPS for frame rate.
val now = getTime()
val delta = now - lastTimeStamp
if (delta < 1.0 / info.fps) {
usleep((1000 * 1000 * (1.0 / info.fps - delta)).toInt())
}
lastTimeStamp = now
}
} else {
// For pure sound, playback is driven by demand.
usleep(10 * 1000)
}
if (decoder.done()) {
state = State.STOPPED
}
}
}
if (hasAudio)
audio.stop()
if (hasVideo)
video.stop()
decoder.stop()
} finally {
input.deinit()
audio.deinit()
video.deinit()
decoder.deinit()
this.deinit()
}
}
}
fun main(args: Array<String>) {
if (args.size < 1) {
println("usage: koplayer file.ext <width> <height>")
exitProcess(1)
}
av_register_all()
val width = if (args.size < 3) 0 else args[1].toInt()
val height = if (args.size < 3) 0 else args[2].toInt()
val player = VideoPlayer(width, height)
player.playFile(args[0])
}