Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,12 @@ internal object LinuxNativeBridge {

// Playback completion
@JvmStatic external fun nConsumeDidPlayToEnd(handle: Long): Boolean

// Stream info and errors

/** Number of video streams, or -1 while the media is not prerolled yet (0 = audio-only). */
@JvmStatic external fun nGetVideoStreamCount(handle: Long): Int

/** Last GStreamer error since the previous call, or null. */
@JvmStatic external fun nConsumeError(handle: Long): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class LinuxVideoPlayerState : VideoPlayerState {

// State tracking
private var lastFrameUpdateTime: Long = 0

// False for audio-only media: no frames will arrive, so frame-based buffering detection doesn't apply.
@Volatile private var hasVideo = true
private var seekInProgress = false
private var targetSeekTime: Double? = null

Expand Down Expand Up @@ -272,7 +275,8 @@ class LinuxVideoPlayerState : VideoPlayerState {
linuxLogger.e { "Failed to open URI" }
withContext(Dispatchers.Main) {
isLoading = false
error = VideoPlayerError.SourceError("Failed to open media source")
// Keep the specific error openMediaUri may have set.
if (error == null) error = VideoPlayerError.SourceError("Failed to open media source")
}
}
} catch (e: Exception) {
Expand Down Expand Up @@ -355,13 +359,22 @@ class LinuxVideoPlayerState : VideoPlayerState {
ptr: Long,
maxAttempts: Int = 20,
) {
hasVideo = true
for (attempt in 1..maxAttempts) {
val width = LinuxNativeBridge.nGetFrameWidth(ptr)
val height = LinuxNativeBridge.nGetFrameHeight(ptr)
if (width > 0 && height > 0) {
linuxLogger.d { "Dimensions validated (w=$width, h=$height) after $attempt attempts" }
return
}
// A source that fails to preroll (unreachable, unsupported, missing decoder) never gets dimensions either.
LinuxNativeBridge.nConsumeError(ptr)?.let { throw IllegalStateException(it) }
// Audio-only media never gets dimensions: stop waiting once the pipeline knows there is no video stream.
if (LinuxNativeBridge.nGetVideoStreamCount(ptr) == 0) {
linuxLogger.d { "Audio-only media (no video stream)" }
hasVideo = false
return
}
linuxLogger.d { "Dimensions not ready yet (attempt $attempt/$maxAttempts)" }
delay(250)
}
Expand Down Expand Up @@ -455,7 +468,7 @@ class LinuxVideoPlayerState : VideoPlayerState {
}

private suspend fun checkBufferingState() {
if (isPlaying && !isLoading) {
if (isPlaying && !isLoading && hasVideo) {
val timeSinceLastFrame = System.currentTimeMillis() - lastFrameUpdateTime
if (timeSinceLastFrame > bufferingTimeoutThreshold) {
withContext(Dispatchers.Main) { isLoading = true }
Expand Down Expand Up @@ -539,10 +552,16 @@ class LinuxVideoPlayerState : VideoPlayerState {
private suspend fun updatePositionAsync() {
if (!hasMedia || userDragging) return
try {
val duration = getDurationSafely()
if (duration <= 0) return
if (reportPendingError()) return

val duration = getDurationSafely()
val current = getPositionSafely()
if (duration <= 0) {
// Unknown duration (e.g. raw ADTS AAC): still show the position and detect the end of media.
withContext(Dispatchers.Main) { _positionText.value = formatTime(current) }
checkLoopingAsync(current, duration)
return
}

withContext(Dispatchers.Main) {
_positionText.value = formatTime(current)
Expand All @@ -567,6 +586,19 @@ class LinuxVideoPlayerState : VideoPlayerState {
}
}

/** Surfaces a GStreamer error (e.g. a missing decoder or a network failure) as the player error. */
private suspend fun reportPendingError(): Boolean {
val ptr = playerPtr
if (ptr == 0L) return false
val message = LinuxNativeBridge.nConsumeError(ptr) ?: return false
linuxLogger.e { "Playback error: $message" }
// Set the error before pausing: pausing cancels the frame update job this usually runs in.
setPlayerError(VideoPlayerError.SourceError(message))
withContext(Dispatchers.Main) { isPlaying = false }
pauseInBackground()
return true
}

private suspend fun checkLoopingAsync(
current: Double,
duration: Double,
Expand Down
31 changes: 31 additions & 0 deletions mediaplayer/src/jvmMain/native/linux/NativeVideoPlayer.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct VideoPlayer {
int32_t audio_channels;
int32_t audio_sample_rate;
float frame_rate;
char* last_error; // pending GStreamer error for Kotlin, consumed by nvp_consume_error

// Playback state
float volume;
Expand Down Expand Up @@ -105,6 +106,10 @@ static void process_bus_message(VideoPlayer* p, GstMessage* msg) {
gst_message_parse_error(msg, &err, &debug);
if (err) {
g_printerr("GStreamer error: %s\n", err->message);
pthread_mutex_lock(&p->meta_lock);
free(p->last_error);
p->last_error = strdup(err->message);
pthread_mutex_unlock(&p->meta_lock);
g_error_free(err);
}
if (debug) g_free(debug);
Expand Down Expand Up @@ -273,6 +278,7 @@ void nvp_destroy(VideoPlayer* p) {
pthread_mutex_lock(&p->meta_lock);
free(p->title);
free(p->mime_type);
free(p->last_error);
pthread_mutex_unlock(&p->meta_lock);
pthread_mutex_destroy(&p->meta_lock);

Expand Down Expand Up @@ -303,6 +309,7 @@ int nvp_open_uri(VideoPlayer* p, const char* uri) {
pthread_mutex_lock(&p->meta_lock);
free(p->title); p->title = NULL;
free(p->mime_type); p->mime_type = NULL;
free(p->last_error); p->last_error = NULL;
p->bitrate = 0;
p->audio_channels = 0;
p->audio_sample_rate = 0;
Expand Down Expand Up @@ -666,3 +673,27 @@ static void update_stream_metadata(VideoPlayer* p) {
}
}
}

// ---------------------------------------------------------------------------
// Stream info and errors
// ---------------------------------------------------------------------------

int32_t nvp_get_video_stream_count(VideoPlayer* p) {
if (!p) return -1;
GstState state = GST_STATE_NULL;
// Timeout 0: the current state; stream counts are only known once prerolled.
gst_element_get_state(p->pipeline, &state, NULL, 0);
if (state < GST_STATE_PAUSED) return -1;
gint n_video = 0;
g_object_get(p->pipeline, "n-video", &n_video, NULL);
return (int32_t)n_video;
}

char* nvp_consume_error(VideoPlayer* p) {
if (!p) return NULL;
pthread_mutex_lock(&p->meta_lock);
char* error = p->last_error;
p->last_error = NULL;
pthread_mutex_unlock(&p->meta_lock);
return error;
}
6 changes: 6 additions & 0 deletions mediaplayer/src/jvmMain/native/linux/NativeVideoPlayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ float nvp_get_frame_rate(VideoPlayer* p);
// End-of-stream notification (consumes the flag)
int32_t nvp_consume_did_play_to_end(VideoPlayer* p);

// Stream info and errors
// Number of video streams, or -1 while the pipeline is not prerolled yet (0 = audio-only).
int32_t nvp_get_video_stream_count(VideoPlayer* p);
// Last GStreamer error since the previous call (caller frees), or NULL.
char* nvp_consume_error(VideoPlayer* p);

#ifdef __cplusplus
}
#endif
Expand Down
15 changes: 15 additions & 0 deletions mediaplayer/src/jvmMain/native/linux/jni_bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ static jstring JNICALL jni_GetVideoTitle(JNIEnv* env, jclass cls, jlong handle)
return result;
}

static jint JNICALL jni_GetVideoStreamCount(JNIEnv* env, jclass cls, jlong handle) {
return handle ? (jint)nvp_get_video_stream_count(toCtx(handle)) : -1;
}

static jstring JNICALL jni_ConsumeError(JNIEnv* env, jclass cls, jlong handle) {
if (!handle) return NULL;
char* s = nvp_consume_error(toCtx(handle));
if (!s) return NULL;
jstring result = (*env)->NewStringUTF(env, s);
free(s);
return result;
}

static jlong JNICALL jni_GetVideoBitrate(JNIEnv* env, jclass cls, jlong handle) {
return handle ? (jlong)nvp_get_bitrate(toCtx(handle)) : 0L;
}
Expand Down Expand Up @@ -161,6 +174,8 @@ static const JNINativeMethod g_methods[] = {
{ "nGetAudioSampleRate", "(J)I", (void*)jni_GetAudioSampleRate },
{ "nGetFrameRate", "(J)F", (void*)jni_GetFrameRate },
{ "nConsumeDidPlayToEnd", "(J)Z", (void*)jni_ConsumeDidPlayToEnd },
{ "nGetVideoStreamCount", "(J)I", (void*)jni_GetVideoStreamCount },
{ "nConsumeError", "(J)Ljava/lang/String;", (void*)jni_ConsumeError },
};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
Expand Down