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
40 changes: 40 additions & 0 deletions .github/workflows/build-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Build APK

on:
push:
branches: [master]
pull_request:

jobs:
build-apk:
name: "Build release APK"
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: recursive

- name: Run setup-libraries composite action
uses: ./lib_ass/src/main/cpp/libass-cmake/.github/actions/setup-libraries
with:
libass-cmake-path: ${{ github.workspace }}/lib_ass/src/main/cpp/libass-cmake

# Gradle 8.14 can't run on the runner's default JDK (25).
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"

- name: Build release APK
run: ./gradlew :app:assembleRelease

- name: Upload APK
uses: actions/upload-artifact@v7
with:
name: demo-libass-android
path: app/build/outputs/apk/release/*.apk
archive: false
if-no-files-found: error
7 changes: 7 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ jobs:
with:
libass-cmake-path: ${{ github.workspace }}/lib_ass/src/main/cpp/libass-cmake

# Gradle 8.14 can't run on the runner's default JDK (25).
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"

- name: Publish to Maven Central Repository
run: ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache
env:
Expand Down
38 changes: 37 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ on:
pull_request:

jobs:
# The EFFECTS_ATLAS overlay's unit tests.
overlay-tests:
name: "Overlay unit tests"
# For Mesa 26.0+: before it, llvmpipe rounds some 8-bit blends down instead of to nearest, which the
# exact blending tests catch.
runs-on: ubuntu-26.04

steps:
- name: Checkout code
uses: actions/checkout@v5

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build pkg-config libass-dev \
libegl-dev libgles-dev libegl-mesa0 libgl1-mesa-dri

- name: Build
run: |
cmake -S lib_ass_kt/src/main/cpp/overlay -B overlay-build -G Ninja
cmake --build overlay-build

- name: Run tests
env:
LIBGL_ALWAYS_SOFTWARE: "1"
# Without a GLES context, the GL tests fail rather than skip.
ASS_OVERLAY_REQUIRE_GL: "1"
run: ctest --test-dir overlay-build --output-on-failure

run-tests:
name: "Test (${{ matrix.arch }})"
runs-on: ubuntu-latest
Expand All @@ -25,6 +54,13 @@ jobs:
with:
libass-cmake-path: ${{ github.workspace }}/lib_ass/src/main/cpp/libass-cmake

# Gradle 8.14 can't run on the runner's default JDK (25).
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
Expand All @@ -34,6 +70,6 @@ jobs:
- name: Run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 21
api-level: 23
arch: ${{ matrix.arch }}
script: ./gradlew test connectedAndroidTest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ google-services.json

# Android Profiling
*.hprof

# cmake build dir
lib_ass_kt/src/main/cpp/overlay/builddir/
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ plugins {

android {
namespace = "io.github.peerless2012.ass.demo"
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "io.github.peerless2012.ass"
minSdk = 21
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
Expand Down
81 changes: 75 additions & 6 deletions app/src/main/java/io/github/peerless2012/ass/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
Expand All @@ -30,10 +32,30 @@ class MainActivity : AppCompatActivity() {

private var url = "http://192.168.3.6:8080/files/c.mkv"

private var renderType = AssRenderType.EFFECTS_ATLAS

private lateinit var player: ExoPlayer

private lateinit var playerView: PlayerView

private lateinit var droppedFramesView: TextView

private val droppedFramesUpdater = object : Runnable {
@OptIn(UnstableApi::class)
override fun run() {
val counters = player.videoDecoderCounters
counters?.ensureUpdated()
droppedFramesView.text = "Dropped frames: ${counters?.droppedBufferCount ?: 0}"
droppedFramesView.postDelayed(this, 500)
}
}

private val openFileLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
playUrl(uri.toString())
}
}

@UnstableApi
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
Expand All @@ -48,10 +70,17 @@ class MainActivity : AppCompatActivity() {
setSupportActionBar(toolbar)
playerView = findViewById(R.id.main_player)

var startPosition = 0*1000L
if (savedInstanceState != null) {
url = savedInstanceState.getString(KEY_URL, url)
renderType = AssRenderType.valueOf(savedInstanceState.getString(KEY_RENDER_TYPE, renderType.name))
startPosition = savedInstanceState.getLong(KEY_POSITION)
}

player = ExoPlayer.Builder(this)
.buildWithAssSupport(
this,
AssRenderType.OVERLAY_OPEN_GL,
renderType,
AssHandlerConfig(maxRenderPixels = 720*480),
playerView.subtitleView
)
Expand Down Expand Up @@ -81,8 +110,11 @@ class MainActivity : AppCompatActivity() {
val mediaItem = MediaItem.Builder()
.setUri(url)
// .setSubtitleConfigurations(ImmutableList.of(enConfig, jpConfig, zhConfig))
player.setMediaItem(mediaItem.build(), 30*1000)
player.setMediaItem(mediaItem.build(), startPosition)
player.prepare()

droppedFramesView = findViewById(R.id.main_dropped_frames)
droppedFramesView.post(droppedFramesUpdater)
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
Expand All @@ -95,6 +127,8 @@ class MainActivity : AppCompatActivity() {
when(item.itemId) {
R.id.menu_gc-> System.gc()
R.id.menu_url-> switchUrl()
R.id.menu_open_file -> openFileLauncher.launch(arrayOf("video/*"))
R.id.menu_render_type -> selectRenderType()
R.id.menu_audio -> selectTrack(C.TRACK_TYPE_AUDIO)
R.id.menu_sub -> selectTrack(C.TRACK_TYPE_TEXT)
R.id.menu_sub_add -> {
Expand Down Expand Up @@ -133,17 +167,38 @@ class MainActivity : AppCompatActivity() {
.setTitle("Set url")
.setPositiveButton("Confirm"
) { dialog, which ->
url = urlInput.text.toString()
player.stop()
player.setMediaItem(MediaItem.fromUri(url))
player.prepare()
playUrl(urlInput.text.toString())
}
.setNegativeButton("Cancel", null)
.setView(urlInput)
.create()
.show()
}

private fun selectRenderType() {
val renderTypes = AssRenderType.entries
AlertDialog.Builder(this)
.setTitle("Render type")
.setSingleChoiceItems(renderTypes.map { it.name }.toTypedArray(), renderTypes.indexOf(renderType)) { dialog, which ->
dialog.dismiss()
if (renderTypes[which] != renderType) {
// The render type is fixed when the player is built, so recreate the activity to rebuild it.
renderType = renderTypes[which]
recreate()
}
}
.setNegativeButton("Cancel", null)
.create()
.show()
}

private fun playUrl(newUrl: String) {
url = newUrl
player.stop()
player.setMediaItem(MediaItem.fromUri(url))
player.prepare()
}

@OptIn(UnstableApi::class)
private fun selectTrack(trackType: Int) {
TrackSelectionDialogBuilder(this, "Select track", player, trackType)
Expand All @@ -152,9 +207,23 @@ class MainActivity : AppCompatActivity() {
.show()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_URL, url)
outState.putString(KEY_RENDER_TYPE, renderType.name)
outState.putLong(KEY_POSITION, player.currentPosition)
}

override fun onDestroy() {
droppedFramesView.removeCallbacks(droppedFramesUpdater)
player.release()
super.onDestroy()
}

companion object {
private const val KEY_URL = "url"
private const val KEY_RENDER_TYPE = "render_type"
private const val KEY_POSITION = "position"
}

}
11 changes: 11 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/main_dropped_frames"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_marginTop="?attr/actionBarSize"
android:background="#80000000"
android:padding="4dp"
android:textColor="#FFFFFF"
android:textSize="14sp"/>

</FrameLayout>
4 changes: 4 additions & 0 deletions app/src/main/res/menu/main_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

<item android:id="@+id/menu_url" android:title="Url"/>

<item android:id="@+id/menu_open_file" android:title="Open File"/>

<item android:id="@+id/menu_render_type" android:title="Render Type"/>

<item android:id="@+id/menu_audio" android:title="Audio"/>

<item android:id="@+id/menu_sub" android:title="Subtitle"/>
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ appcompat = "1.7.1"
material = "1.12.0"
activity = "1.10.1"
constraintlayout = "2.2.1"
media3 = "1.8.0"
media3 = "1.11.1"
publish = "0.34.0"

[libraries]
Expand Down
2 changes: 1 addition & 1 deletion lib_ass/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {
ndkVersion = "28.1.13356709"

defaultConfig {
minSdk = 21
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
externalNativeBuild {
Expand Down
2 changes: 1 addition & 1 deletion lib_ass_kt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {
ndkVersion = "28.1.13356709"

defaultConfig {
minSdk = 21
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
externalNativeBuild {
Expand Down
14 changes: 13 additions & 1 deletion lib_ass_kt/src/main/cpp/AssKt.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jni.h>
#include "ass/ass.h"
#include "fontconfig/fontconfig.h"
#include "GLES2/gl2.h"
#include "GLES2/gl2ext.h"
#include "AssOverlay.h"

#define LOG_TAG "SubtitleRenderer"

Expand Down Expand Up @@ -361,6 +361,16 @@ void nativeAssRenderDeinit(JNIEnv* env, jclass clazz, jlong render) {
}
}

jlong nativeAssOverlayDraw(JNIEnv* env, jclass clazz, jlong overlay, jlong render, jlong track,
jint fbo, jint frameWidth, jint frameHeight, jlong timeMs) {
return (jlong) assOverlayDraw((AssOverlay *) overlay, (ASS_Renderer *) render, (ASS_Track *) track,
(unsigned int) fbo, frameWidth, frameHeight, timeMs);
}

void nativeAssOverlayRelease(JNIEnv* env, jclass clazz, jlong overlay) {
assOverlayRelease((AssOverlay *) overlay);
}

static JNINativeMethod renderMethodTable[] = {
{"nativeAssRenderInit", "(J)J", (void*)nativeAssRenderInit},
{"nativeAssRenderSetFontScale", "(JF)V", (void*)nativeAssRenderSetFontScale},
Expand All @@ -369,6 +379,8 @@ static JNINativeMethod renderMethodTable[] = {
{"nativeAssRenderSetFrameSize", "(JII)V", (void*)nativeAssRenderSetFrameSize},
{"nativeAssRenderFrame", "(JJJI)Lio/github/peerless2012/ass/AssFrame;", (void*) nativeAssRenderFrame},
{"nativeAssRenderDeinit", "(J)V", (void*)nativeAssRenderDeinit},
{"nativeAssOverlayDraw", "(JJJIIIJ)J", (void*) nativeAssOverlayDraw},
{"nativeAssOverlayRelease", "(J)V", (void*) nativeAssOverlayRelease},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
Expand Down
4 changes: 3 additions & 1 deletion lib_ass_kt/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ include_directories(${CMAKE_SOURCE_DIR}/include)
add_library(${CMAKE_PROJECT_NAME} SHARED AssKt.c)
# Add these two lines.
find_package(lib_ass REQUIRED CONFIG)
add_subdirectory(overlay)
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME "asskt")
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE
lib_ass::ass
ass_overlay
android
jnigraphics
GLESv2
z
log)
log)
Loading
Loading