From f952bc6665c6b571d89271eae8e716814b33491d Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:35:00 +0100 Subject: [PATCH 1/2] Add thread safety and lifecycle for native pointers --- lib_ass_kt/src/main/cpp/AssKt.c | 12 ++++++ .../java/io/github/peerless2012/ass/Ass.kt | 23 ++++++++-- .../io/github/peerless2012/ass/AssRender.kt | 42 ++++++++++++++++--- .../io/github/peerless2012/ass/AssTrack.kt | 26 ++++++++++-- .../peerless2012/ass/media/AssHandler.kt | 34 +++++++++++++-- 5 files changed, 123 insertions(+), 14 deletions(-) diff --git a/lib_ass_kt/src/main/cpp/AssKt.c b/lib_ass_kt/src/main/cpp/AssKt.c index 6110a16..7ecfe72 100644 --- a/lib_ass_kt/src/main/cpp/AssKt.c +++ b/lib_ass_kt/src/main/cpp/AssKt.c @@ -69,10 +69,12 @@ jlong nativeAssTrackInit(JNIEnv* env, jclass clazz, jlong ass) { } jint nativeAssTrackGetWidth(JNIEnv* env, jclass clazz, jlong track) { + if (!track) return 0; return ((ASS_Track *) track)->PlayResX; } jobjectArray nativeAssTrackGetEvents(JNIEnv* env, jclass clazz, jlong track) { + if (!track) return NULL; jclass eventClass = (*env)->FindClass(env, "io/github/peerless2012/ass/AssEvent"); if (eventClass == NULL) { return NULL; @@ -121,6 +123,7 @@ jobjectArray nativeAssTrackGetEvents(JNIEnv* env, jclass clazz, jlong track) { } void nativeAssTrackClearEvents(JNIEnv* env, jclass clazz, jlong track) { + if (!track) return; ASS_Track* tr = (ASS_Track *) track; for (int i = 0; i < tr->n_events; i++) { ass_free_event(tr, i); @@ -129,10 +132,12 @@ void nativeAssTrackClearEvents(JNIEnv* env, jclass clazz, jlong track) { } jint nativeAssTrackGetHeight(JNIEnv* env, jclass clazz, jlong track) { + if (!track) return 0; return ((ASS_Track *) track)->PlayResY; } void nativeAssTrackReadBuffer(JNIEnv* env, jclass clazz, jlong track, jbyteArray buffer, jint offset, jint length) { + if (!track) return; jboolean isCopy; jbyte* elements = (*env)->GetByteArrayElements(env, buffer, &isCopy); if (elements == NULL) { @@ -143,6 +148,7 @@ void nativeAssTrackReadBuffer(JNIEnv* env, jclass clazz, jlong track, jbyteArray } void nativeAssTrackReadChunk(JNIEnv* env, jclass clazz, jlong track, jlong start, jlong duration, jbyteArray buffer, jint offset, jint length) { + if (!track) return; jboolean isCopy; jbyte* elements = (*env)->GetByteArrayElements(env, buffer, &isCopy); if (elements == NULL) { @@ -153,6 +159,7 @@ void nativeAssTrackReadChunk(JNIEnv* env, jclass clazz, jlong track, jlong start } void nativeAssTrackDeinit(JNIEnv* env, jclass clazz, jlong track) { + if (!track) return; ass_free_track((ASS_Track *) track); } @@ -175,18 +182,22 @@ jlong nativeAssRenderInit(JNIEnv* env, jclass clazz, jlong ass) { } void nativeAssRenderSetFontScale(JNIEnv* env, jclass clazz, jlong render, jfloat scale) { + if (!render) return; ass_set_font_scale((ASS_Renderer *) render, scale); } void nativeAssRenderSetCacheLimit(JNIEnv* env, jclass clazz, jlong render, jint glyphMax, jint bitmapMaxSize) { + if (!render) return; ass_set_cache_limits((ASS_Renderer *) render, glyphMax, bitmapMaxSize); } void nativeAssRenderSetFrameSize(JNIEnv* env, jclass clazz, jlong render, jint width, jint height) { + if (!render) return; ass_set_frame_size((ASS_Renderer *) render, width, height); } void nativeAssRenderSetStorageSize(JNIEnv* env, jclass clazz, jlong render, jint width, jint height) { + if (!render) return; ass_set_storage_size((ASS_Renderer *) render, width, height); } @@ -294,6 +305,7 @@ static int count_ass_images(ASS_Image *images) { } jobject nativeAssRenderFrame(JNIEnv* env, jclass clazz, jlong render, jlong track, jlong time, jint type) { + if (!render || !track) return NULL; int changed; ASS_Image *image = ass_render_frame((ASS_Renderer *) render, (ASS_Track *) track, time, &changed); if (image == NULL) { diff --git a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt index c59e13b..3ec5d6d 100644 --- a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt +++ b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt @@ -29,26 +29,43 @@ class Ass { } - private val nativeAss: Long = nativeAssInit() + private var nativeAss: Long = nativeAssInit() + + @Volatile + var released = false + private set public fun createTrack(): AssTrack { + if (released || nativeAss == 0L) throw IllegalStateException("Ass already released") return AssTrack(nativeAss) } public fun createRender(): AssRender { + if (released || nativeAss == 0L) throw IllegalStateException("Ass already released") return AssRender(nativeAss) } public fun addFont(name: String, buffer: ByteArray) { + if (released || nativeAss == 0L) return nativeAssAddFont(nativeAss, name, buffer) } public fun clearFont() { + if (released || nativeAss == 0L) return nativeAssClearFont(nativeAss) } + fun release() { + if (released) return + released = true + if (nativeAss != 0L) { + nativeAssDeinit(nativeAss) + nativeAss = 0 + } + } + protected fun finalize() { - nativeAssDeinit(nativeAss) + release() } -} \ No newline at end of file +} diff --git a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt index 4edc60c..fbb5a1b 100644 --- a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt +++ b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt @@ -1,5 +1,8 @@ package io.github.peerless2012.ass +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + /** * @Author peerless2012 * @Email peerless2012@126.com @@ -33,36 +36,65 @@ class AssRender(nativeAss: Long) { external fun nativeAssRenderDeinit(render: Long) } - private val nativeRender: Long = nativeAssRenderInit(nativeAss) + val lock = ReentrantLock() + + private var nativeRender: Long = nativeAssRenderInit(nativeAss) + + @Volatile + var released = false + private set private var track: AssTrack? = null public fun setTrack(track: AssTrack?) { - this.track = track + lock.withLock { + this.track = track + } } public fun setFontScale(scale: Float) { + if (released || nativeRender == 0L) return nativeAssRenderSetFontScale(nativeRender, scale) } public fun setCacheLimit(glyphMax: Int, bitmapMaxSize: Int) { + if (released || nativeRender == 0L) return nativeAssRenderSetCacheLimit(nativeRender, glyphMax, bitmapMaxSize) } public fun setStorageSize(width: Int, height: Int) { + if (released || nativeRender == 0L) return nativeAssRenderSetStorageSize(nativeRender, width, height) } public fun setFrameSize(width: Int, height: Int) { + if (released || nativeRender == 0L) return nativeAssRenderSetFrameSize(nativeRender, width, height) } public fun renderFrame(time: Long, type: AssTexType): AssFrame? { - return track?.let { nativeAssRenderFrame(nativeRender, it.nativeAssTrack, time, type.ordinal) } + lock.withLock { + if (released || nativeRender == 0L) return null + val t = track ?: return null + if (t.released || t.nativeAssTrack == 0L) return null + return nativeAssRenderFrame(nativeRender, t.nativeAssTrack, time, type.ordinal) + } + } + + fun release() { + lock.withLock { + if (released) return + released = true + track = null + if (nativeRender != 0L) { + nativeAssRenderDeinit(nativeRender) + nativeRender = 0 + } + } } protected fun finalize() { - nativeAssRenderDeinit(nativeRender) + release() } -} \ No newline at end of file +} diff --git a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt index 8ab1497..9a9a3b4 100644 --- a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt +++ b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt @@ -36,34 +36,54 @@ class AssTrack(private val ass: Long) { external fun nativeAssTrackDeinit(track: Long) } - public val nativeAssTrack = nativeAssTrackInit(ass) + var nativeAssTrack = nativeAssTrackInit(ass) + private set + + @Volatile + var released = false + private set public fun getWidth(): Int { + if (released || nativeAssTrack == 0L) return 0 return nativeAssTrackGetWidth(nativeAssTrack) } public fun getHeight(): Int { + if (released || nativeAssTrack == 0L) return 0 return nativeAssTrackGetHeight(nativeAssTrack) } public fun getEvents(): Array? { + if (released || nativeAssTrack == 0L) return null return nativeAssTrackGetEvents(nativeAssTrack) } public fun clearEvent() { + if (released || nativeAssTrack == 0L) return nativeAssTrackClearEvents(nativeAssTrack) } public fun readBuffer(array: ByteArray, offset: Int = 0, length : Int = array.size) { + if (released || nativeAssTrack == 0L) return nativeAssTrackReadBuffer(nativeAssTrack, array, offset, length) } public fun readChunk(start: Long, duration: Long, array: ByteArray, offset: Int = 0, length: Int = array.size) { + if (released || nativeAssTrack == 0L) return nativeAssTrackReadChunk(nativeAssTrack, start, duration, array, offset, length) } + fun release() { + if (released) return + released = true + if (nativeAssTrack != 0L) { + nativeAssTrackDeinit(nativeAssTrack) + nativeAssTrack = 0 + } + } + protected fun finalize() { - nativeAssTrackDeinit(nativeAssTrack) + release() } -} \ No newline at end of file +} diff --git a/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt b/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt index b074014..d7b300a 100644 --- a/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt +++ b/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt @@ -19,6 +19,7 @@ import io.github.peerless2012.ass.Ass import io.github.peerless2012.ass.media.parser.AssHeaderParser import io.github.peerless2012.ass.media.render.AssOverlayManager import io.github.peerless2012.ass.media.type.AssRenderType +import kotlin.concurrent.withLock /** * Handles ASS subtitle rendering and integration with ExoPlayer. @@ -35,7 +36,8 @@ class AssHandler( /** The ASS instance used for creating tracks and renderers. This is lazy to avoid loading * libass if the played media does not have ASS tracks. */ - val ass by lazy { Ass() } + private val assDelegate = lazy { Ass() } + val ass by assDelegate /** The current ASS renderer. It's created as soon as a ASS track is detected. */ var render: AssRender? = null @@ -296,7 +298,8 @@ class AssHandler( /** * Reads a dialogue into the track of the given [trackId]. - * TODO This should move to executor. + * Synchronized with renderFrame via the render lock to prevent concurrent + * ass_process_chunk / ass_render_frame on the same track. */ fun readTrackDialogue( trackId: String?, @@ -306,7 +309,15 @@ class AssHandler( offset: Int = 0, length: Int = data.size ) { - availableTracks[trackId]?.readChunk(start, duration, data, offset, length) + val t = availableTracks[trackId] ?: return + val r = render + if (r != null) { + r.lock.withLock { + t.readChunk(start, duration, data, offset, length) + } + } else { + t.readChunk(start, duration, data, offset, length) + } } /** @@ -343,6 +354,23 @@ class AssHandler( }?.getTrackFormat(0) } + /** + * Releases all native resources held by this handler. + */ + fun release() { + videoTimeCallback = null + overlayManager?.disable() + render?.release() + render = null + availableTracks.values.forEach { it.release() } + availableTracks.clear() + track = null + pendingFonts.clear() + if (assDelegate.isInitialized()) { + ass.release() + } + } + /** * Checks if the size is valid (both width and height are greater than 0). */ From 09b36ca765c59e7de3d897d0d3bf66c2705cdfe2 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:29:40 +0100 Subject: [PATCH 2/2] Use single lock per ASS_Library for all libass calls --- .../java/io/github/peerless2012/ass/Ass.kt | 42 ++++++++++----- .../io/github/peerless2012/ass/AssRender.kt | 28 ++++++---- .../io/github/peerless2012/ass/AssTrack.kt | 53 ++++++++++++------- .../peerless2012/ass/media/AssHandler.kt | 13 +---- 4 files changed, 83 insertions(+), 53 deletions(-) diff --git a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt index 3ec5d6d..bcc56a6 100644 --- a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt +++ b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/Ass.kt @@ -1,5 +1,8 @@ package io.github.peerless2012.ass +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + /** * @Author peerless2012 * @Email peerless2012@126.com @@ -29,6 +32,9 @@ class Ass { } + /** Single lock for all libass calls on this library instance. */ + val lock = ReentrantLock() + private var nativeAss: Long = nativeAssInit() @Volatile @@ -36,31 +42,41 @@ class Ass { private set public fun createTrack(): AssTrack { - if (released || nativeAss == 0L) throw IllegalStateException("Ass already released") - return AssTrack(nativeAss) + return lock.withLock { + if (released || nativeAss == 0L) throw IllegalStateException("Ass already released") + AssTrack(nativeAss, lock) + } } public fun createRender(): AssRender { - if (released || nativeAss == 0L) throw IllegalStateException("Ass already released") - return AssRender(nativeAss) + return lock.withLock { + if (released || nativeAss == 0L) throw IllegalStateException("Ass already released") + AssRender(nativeAss, lock) + } } public fun addFont(name: String, buffer: ByteArray) { - if (released || nativeAss == 0L) return - nativeAssAddFont(nativeAss, name, buffer) + lock.withLock { + if (released || nativeAss == 0L) return + nativeAssAddFont(nativeAss, name, buffer) + } } public fun clearFont() { - if (released || nativeAss == 0L) return - nativeAssClearFont(nativeAss) + lock.withLock { + if (released || nativeAss == 0L) return + nativeAssClearFont(nativeAss) + } } fun release() { - if (released) return - released = true - if (nativeAss != 0L) { - nativeAssDeinit(nativeAss) - nativeAss = 0 + lock.withLock { + if (released) return + released = true + if (nativeAss != 0L) { + nativeAssDeinit(nativeAss) + nativeAss = 0 + } } } diff --git a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt index fbb5a1b..877cbdf 100644 --- a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt +++ b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssRender.kt @@ -10,7 +10,7 @@ import kotlin.concurrent.withLock * @Version V1.0 * @Description */ -class AssRender(nativeAss: Long) { +class AssRender(nativeAss: Long, private val lock: ReentrantLock) { companion object { @@ -36,8 +36,6 @@ class AssRender(nativeAss: Long) { external fun nativeAssRenderDeinit(render: Long) } - val lock = ReentrantLock() - private var nativeRender: Long = nativeAssRenderInit(nativeAss) @Volatile @@ -53,23 +51,31 @@ class AssRender(nativeAss: Long) { } public fun setFontScale(scale: Float) { - if (released || nativeRender == 0L) return - nativeAssRenderSetFontScale(nativeRender, scale) + lock.withLock { + if (released || nativeRender == 0L) return + nativeAssRenderSetFontScale(nativeRender, scale) + } } public fun setCacheLimit(glyphMax: Int, bitmapMaxSize: Int) { - if (released || nativeRender == 0L) return - nativeAssRenderSetCacheLimit(nativeRender, glyphMax, bitmapMaxSize) + lock.withLock { + if (released || nativeRender == 0L) return + nativeAssRenderSetCacheLimit(nativeRender, glyphMax, bitmapMaxSize) + } } public fun setStorageSize(width: Int, height: Int) { - if (released || nativeRender == 0L) return - nativeAssRenderSetStorageSize(nativeRender, width, height) + lock.withLock { + if (released || nativeRender == 0L) return + nativeAssRenderSetStorageSize(nativeRender, width, height) + } } public fun setFrameSize(width: Int, height: Int) { - if (released || nativeRender == 0L) return - nativeAssRenderSetFrameSize(nativeRender, width, height) + lock.withLock { + if (released || nativeRender == 0L) return + nativeAssRenderSetFrameSize(nativeRender, width, height) + } } public fun renderFrame(time: Long, type: AssTexType): AssFrame? { diff --git a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt index 9a9a3b4..5500100 100644 --- a/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt +++ b/lib_ass_kt/src/main/java/io/github/peerless2012/ass/AssTrack.kt @@ -1,5 +1,8 @@ package io.github.peerless2012.ass +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + /** * @Author peerless2012 * @Email peerless2012@126.com @@ -7,7 +10,7 @@ package io.github.peerless2012.ass * @Version V1.0 * @Description */ -class AssTrack(private val ass: Long) { +class AssTrack(private val ass: Long, private val lock: ReentrantLock) { companion object { @@ -44,41 +47,55 @@ class AssTrack(private val ass: Long) { private set public fun getWidth(): Int { - if (released || nativeAssTrack == 0L) return 0 - return nativeAssTrackGetWidth(nativeAssTrack) + lock.withLock { + if (released || nativeAssTrack == 0L) return 0 + return nativeAssTrackGetWidth(nativeAssTrack) + } } public fun getHeight(): Int { - if (released || nativeAssTrack == 0L) return 0 - return nativeAssTrackGetHeight(nativeAssTrack) + lock.withLock { + if (released || nativeAssTrack == 0L) return 0 + return nativeAssTrackGetHeight(nativeAssTrack) + } } public fun getEvents(): Array? { - if (released || nativeAssTrack == 0L) return null - return nativeAssTrackGetEvents(nativeAssTrack) + lock.withLock { + if (released || nativeAssTrack == 0L) return null + return nativeAssTrackGetEvents(nativeAssTrack) + } } public fun clearEvent() { - if (released || nativeAssTrack == 0L) return - nativeAssTrackClearEvents(nativeAssTrack) + lock.withLock { + if (released || nativeAssTrack == 0L) return + nativeAssTrackClearEvents(nativeAssTrack) + } } public fun readBuffer(array: ByteArray, offset: Int = 0, length : Int = array.size) { - if (released || nativeAssTrack == 0L) return - nativeAssTrackReadBuffer(nativeAssTrack, array, offset, length) + lock.withLock { + if (released || nativeAssTrack == 0L) return + nativeAssTrackReadBuffer(nativeAssTrack, array, offset, length) + } } public fun readChunk(start: Long, duration: Long, array: ByteArray, offset: Int = 0, length: Int = array.size) { - if (released || nativeAssTrack == 0L) return - nativeAssTrackReadChunk(nativeAssTrack, start, duration, array, offset, length) + lock.withLock { + if (released || nativeAssTrack == 0L) return + nativeAssTrackReadChunk(nativeAssTrack, start, duration, array, offset, length) + } } fun release() { - if (released) return - released = true - if (nativeAssTrack != 0L) { - nativeAssTrackDeinit(nativeAssTrack) - nativeAssTrack = 0 + lock.withLock { + if (released) return + released = true + if (nativeAssTrack != 0L) { + nativeAssTrackDeinit(nativeAssTrack) + nativeAssTrack = 0 + } } } diff --git a/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt b/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt index d7b300a..b0e9779 100644 --- a/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt +++ b/lib_ass_media/src/main/java/io/github/peerless2012/ass/media/AssHandler.kt @@ -19,7 +19,6 @@ import io.github.peerless2012.ass.Ass import io.github.peerless2012.ass.media.parser.AssHeaderParser import io.github.peerless2012.ass.media.render.AssOverlayManager import io.github.peerless2012.ass.media.type.AssRenderType -import kotlin.concurrent.withLock /** * Handles ASS subtitle rendering and integration with ExoPlayer. @@ -298,8 +297,7 @@ class AssHandler( /** * Reads a dialogue into the track of the given [trackId]. - * Synchronized with renderFrame via the render lock to prevent concurrent - * ass_process_chunk / ass_render_frame on the same track. + * Thread-safe: AssTrack.readChunk internally acquires the shared libass lock. */ fun readTrackDialogue( trackId: String?, @@ -310,14 +308,7 @@ class AssHandler( length: Int = data.size ) { val t = availableTracks[trackId] ?: return - val r = render - if (r != null) { - r.lock.withLock { - t.readChunk(start, duration, data, offset, length) - } - } else { - t.readChunk(start, duration, data, offset, length) - } + t.readChunk(start, duration, data, offset, length) } /**