Sfoglia il codice sorgente

First working version.

jcsyshc 2 anni fa
commit
13be21ddc4
33 ha cambiato i file con 756 aggiunte e 0 eliminazioni
  1. 1 0
      app/.gitignore
  2. 70 0
      app/build.gradle.kts
  3. 21 0
      app/proguard-rules.pro
  4. 24 0
      app/src/androidTest/java/icu/shcno/tinyplayerandroid/ExampleInstrumentedTest.kt
  5. 35 0
      app/src/main/AndroidManifest.xml
  6. 88 0
      app/src/main/java/icu/shcno/tinyplayerandroid/MainActivity.kt
  7. 46 0
      app/src/main/java/icu/shcno/tinyplayerandroid/PlayerActivity.kt
  8. 6 0
      app/src/main/java/icu/shcno/tinyplayerandroid/data/ServerInfo.kt
  9. 133 0
      app/src/main/java/icu/shcno/tinyplayerandroid/ui/VideoDecoder.kt
  10. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  11. 30 0
      app/src/main/res/drawable/ic_launcher_foreground.xml
  12. 6 0
      app/src/main/res/mipmap-anydpi/ic_launcher.xml
  13. 6 0
      app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
  14. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  15. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  16. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  17. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  18. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  19. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  20. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  21. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  22. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  23. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  24. 10 0
      app/src/main/res/values/colors.xml
  25. 5 0
      app/src/main/res/values/strings.xml
  26. 5 0
      app/src/main/res/values/themes.xml
  27. 13 0
      app/src/main/res/xml/backup_rules.xml
  28. 19 0
      app/src/main/res/xml/data_extraction_rules.xml
  29. 17 0
      app/src/test/java/icu/shcno/tinyplayerandroid/ExampleUnitTest.kt
  30. 5 0
      build.gradle.kts
  31. 23 0
      gradle.properties
  32. 6 0
      gradle/wrapper/gradle-wrapper.properties
  33. 17 0
      settings.gradle.kts

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 70 - 0
app/build.gradle.kts

@@ -0,0 +1,70 @@
+plugins {
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    namespace = "icu.shcno.tinyplayerandroid"
+    compileSdk = 34
+
+    defaultConfig {
+        applicationId = "icu.shcno.tinyplayerandroid"
+        minSdk = 29
+        targetSdk = 34
+        versionCode = 1
+        versionName = "1.0"
+
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        vectorDrawables {
+            useSupportLibrary = true
+        }
+    }
+
+    buildTypes {
+        release {
+            isMinifyEnabled = false
+            proguardFiles(
+                getDefaultProguardFile("proguard-android-optimize.txt"),
+                "proguard-rules.pro"
+            )
+        }
+    }
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+    buildFeatures {
+        compose = true
+    }
+    composeOptions {
+        kotlinCompilerExtensionVersion = "1.5.1"
+    }
+    packaging {
+        resources {
+            excludes += "/META-INF/{AL2.0,LGPL2.1}"
+        }
+    }
+}
+
+dependencies {
+
+    implementation("androidx.core:core-ktx:1.12.0")
+    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
+    implementation("androidx.activity:activity-compose:1.8.2")
+    implementation(platform("androidx.compose:compose-bom:2023.08.00"))
+    implementation("androidx.compose.ui:ui")
+    implementation("androidx.compose.ui:ui-graphics")
+    implementation("androidx.compose.ui:ui-tooling-preview")
+    implementation("androidx.compose.material3:material3")
+    implementation("androidx.navigation:navigation-compose:2.7.6")
+    testImplementation("junit:junit:4.13.2")
+    androidTestImplementation("androidx.test.ext:junit:1.1.5")
+    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+    androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
+    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+    debugImplementation("androidx.compose.ui:ui-tooling")
+    debugImplementation("androidx.compose.ui:ui-test-manifest")
+}

+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 24 - 0
app/src/androidTest/java/icu/shcno/tinyplayerandroid/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package icu.shcno.tinyplayerandroid
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("icu.shcno.tinyplayerandroid", appContext.packageName)
+    }
+}

+ 35 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+    <application
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.TinyPlayerAndroid"
+        tools:targetApi="31">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.TinyPlayerAndroid">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".PlayerActivity"
+            android:parentActivityName=".MainActivity"
+            android:screenOrientation="landscape"
+            android:theme="@android:style/Theme.NoTitleBar.Fullscreen"/>
+    </application>
+
+</manifest>

+ 88 - 0
app/src/main/java/icu/shcno/tinyplayerandroid/MainActivity.kt

@@ -0,0 +1,88 @@
+package icu.shcno.tinyplayerandroid
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+class MainActivity : ComponentActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContent {
+            Surface(
+                modifier = Modifier.fillMaxSize()
+            ) {
+                ControllerPanel()
+            }
+        }
+    }
+
+    @Composable
+    fun ControllerPanel() {
+        val context = LocalContext.current
+
+        val padding = 5.dp
+
+        var serverIp by remember { mutableStateOf("192.168.1.117") }
+        var serverPort by remember { mutableStateOf("5279") }
+
+        val isValidServerPort = serverPort.toIntOrNull() in 1..65535
+
+        Column(
+            modifier = Modifier.padding(padding), horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            OutlinedTextField(
+                value = serverIp,
+                onValueChange = { serverIp = it },
+                label = { Text(text = "Server IP") },
+                isError = serverIp.isEmpty()
+            )
+            Spacer(modifier = Modifier.padding(padding))
+            OutlinedTextField(
+                value = serverPort,
+                onValueChange = { serverPort = it },
+                label = { Text(text = "Server Port") },
+                isError = !isValidServerPort
+            )
+            Spacer(modifier = Modifier.padding(padding))
+            Button(
+                onClick = {
+                    val intent = Intent(context, PlayerActivity::class.java)
+                    intent.putExtra("ServerIp", serverIp)
+                    intent.putExtra("ServerPort", serverPort.toInt())
+                    startActivity(intent)
+                }) {
+                Text(text = "Start")
+            }
+        }
+    }
+
+    @Preview(showBackground = true)
+    @Composable
+    fun ControllerPreview() {
+        ControllerPanel()
+    }
+
+}
+
+

+ 46 - 0
app/src/main/java/icu/shcno/tinyplayerandroid/PlayerActivity.kt

@@ -0,0 +1,46 @@
+package icu.shcno.tinyplayerandroid
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.SurfaceHolder
+import android.view.SurfaceView
+import android.view.WindowManager
+import icu.shcno.tinyplayerandroid.data.ServerInfo
+import icu.shcno.tinyplayerandroid.ui.VideoDecoder
+
+class PlayerActivity : Activity(), SurfaceHolder.Callback {
+
+    private var decoderThread: Thread? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // keep screen on
+        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+
+        SurfaceView(this).apply {
+            holder.addCallback(this@PlayerActivity)
+            setContentView(this)
+        }
+    }
+
+    override fun surfaceCreated(holder: SurfaceHolder) {
+        // TODO
+    }
+
+    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
+        if (decoderThread != null) return
+
+        // create decoder thread
+        val serverInfo = ServerInfo(
+            intent.getStringExtra("ServerIp") ?: return,
+            intent.getIntExtra("ServerPort", 0)
+        )
+        decoderThread = Thread(VideoDecoder(this, serverInfo, holder.surface))
+        decoderThread?.start()
+    }
+
+    override fun surfaceDestroyed(holder: SurfaceHolder) {
+        // TODO
+    }
+}

+ 6 - 0
app/src/main/java/icu/shcno/tinyplayerandroid/data/ServerInfo.kt

@@ -0,0 +1,6 @@
+package icu.shcno.tinyplayerandroid.data
+
+data class ServerInfo(
+    val ip: String,
+    val port: Int
+)

+ 133 - 0
app/src/main/java/icu/shcno/tinyplayerandroid/ui/VideoDecoder.kt

@@ -0,0 +1,133 @@
+package icu.shcno.tinyplayerandroid.ui
+
+import android.content.Context
+import android.media.MediaCodec
+import android.media.MediaCodec.BufferInfo
+import android.media.MediaCodecList
+import android.media.MediaFormat
+import android.util.Log
+import android.view.Surface
+import icu.shcno.tinyplayerandroid.R
+import icu.shcno.tinyplayerandroid.data.ServerInfo
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.net.Socket
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+class NoDecoderException(msg: String) : Exception(msg)
+
+class VideoDecoder(
+    private val context: Context,
+    private val serverInfo: ServerInfo,
+    private val surface: Surface
+) : Runnable {
+
+    companion object {
+        const val Tag = "VideoDecoder"
+        const val VideoFormatMIME = MediaFormat.MIMETYPE_VIDEO_HEVC
+        const val VideoWidth = 1920
+        const val VideoHeight = 1080
+
+        fun readFully(inputStream: InputStream, n: Int): ByteArray? {
+            val readBuffer = ByteArray(DEFAULT_BUFFER_SIZE)
+            val outputStream = ByteArrayOutputStream()
+            var bytesLeft = n
+            while (bytesLeft > 0) {
+                val bytesToRead = minOf(readBuffer.size, bytesLeft)
+                val bytesRead = inputStream.read(readBuffer, 0, bytesToRead)
+                if (bytesRead == -1) return null
+                outputStream.write(readBuffer, 0, bytesRead)
+                bytesLeft -= bytesRead
+            }
+            return outputStream.toByteArray()
+        }
+    }
+
+    override fun run() {
+        // find a hardware accelerated H.265 decoder
+        val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
+        var decoderName: String? = null
+        loop@ for (codec in codecList.codecInfos) {
+            if (codec.isEncoder) continue
+            if (!codec.isHardwareAccelerated) continue
+            for (type in codec.supportedTypes) {
+                if (type == VideoFormatMIME) {
+                    decoderName = codec.name
+                    break@loop
+                }
+            }
+        }
+        if (decoderName == null) {
+            Log.e(Tag, context.getString(R.string.no_decoder))
+            throw NoDecoderException(context.getString(R.string.no_decoder))
+        } else {
+            Log.i(Tag, context.getString(R.string.use_decoder, decoderName))
+        }
+
+        // create and config decoder
+        val decoder = MediaCodec.createByCodecName(decoderName)
+        decoder.configure(
+            MediaFormat.createVideoFormat(
+                VideoFormatMIME, VideoWidth, VideoHeight
+            ),
+            surface, null, 0
+        )
+        decoder.start()
+
+        val socket = Socket(serverInfo.ip, serverInfo.port)
+        val inputStream = socket.getInputStream()
+        var isFirst = true
+        while (true) {
+            // read encoded frame data
+            val lengthByteArray = readFully(inputStream, ULong.SIZE_BYTES) ?: break
+            val length = ByteBuffer.wrap(lengthByteArray)
+                .order(ByteOrder.BIG_ENDIAN).getLong().toInt()
+            val packetData = readFully(inputStream, length) ?: break
+
+            // read frame id
+            val frameId = ByteBuffer.wrap(packetData, 0, ULong.SIZE_BYTES)
+                .order(ByteOrder.BIG_ENDIAN).getLong().toInt()
+            val frameData = packetData.copyOfRange(ULong.SIZE_BYTES, length)
+            Log.d(Tag, "Received frame $frameId, length = ${frameData.size}")
+
+            // queue encoded frame
+            val inputIndex = decoder.dequeueInputBuffer(-1)
+            val inputBuffer = decoder.getInputBuffer(inputIndex) ?: break
+            assert(inputBuffer.position() == 0)
+            inputBuffer.put(frameData)
+            val currentTs = System.currentTimeMillis() * 1000 // ms -> us
+            decoder.queueInputBuffer(
+                inputIndex, 0, frameData.size,
+                currentTs, if (isFirst) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
+            )
+            isFirst = false
+
+            // retrieve decoded frame
+            val bufferInfo = BufferInfo()
+            while (true) {
+                val outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 1000)
+                when (outputIndex) {
+
+                    MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
+                        // TODO
+                    }
+
+                    MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
+                        // TODO
+                    }
+
+                    MediaCodec.INFO_TRY_AGAIN_LATER -> {
+                        break
+                        // TODO
+                    }
+
+                    else -> {
+                        assert(outputIndex >= 0)
+                        decoder.releaseOutputBuffer(outputIndex, true)
+                    }
+                }
+            }
+        }
+    }
+}

+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 30 - 0
app/src/main/res/drawable/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

+ 6 - 0
app/src/main/res/mipmap-anydpi/ic_launcher.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 6 - 0
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 10 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

+ 5 - 0
app/src/main/res/values/strings.xml

@@ -0,0 +1,5 @@
+<resources>
+    <string name="app_name">TinyPlayerAndroid</string>
+    <string name="no_decoder">No valid H.265 decoder found.</string>
+    <string name="use_decoder">Use %1$s as video stream decoder.</string>
+</resources>

+ 5 - 0
app/src/main/res/values/themes.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="Theme.TinyPlayerAndroid" parent="android:Theme.Material.Light.NoActionBar" />
+</resources>

+ 13 - 0
app/src/main/res/xml/backup_rules.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample backup rules file; uncomment and customize as necessary.
+   See https://developer.android.com/guide/topics/data/autobackup
+   for details.
+   Note: This file is ignored for devices older that API 31
+   See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+    <!--
+   <include domain="sharedpref" path="."/>
+   <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>

+ 19 - 0
app/src/main/res/xml/data_extraction_rules.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample data extraction rules file; uncomment and customize as necessary.
+   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+   for details.
+-->
+<data-extraction-rules>
+    <cloud-backup>
+        <!-- TODO: Use <include> and <exclude> to control what is backed up.
+        <include .../>
+        <exclude .../>
+        -->
+    </cloud-backup>
+    <!--
+    <device-transfer>
+        <include .../>
+        <exclude .../>
+    </device-transfer>
+    -->
+</data-extraction-rules>

+ 17 - 0
app/src/test/java/icu/shcno/tinyplayerandroid/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package icu.shcno.tinyplayerandroid
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}

+ 5 - 0
build.gradle.kts

@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+    id("com.android.application") version "8.2.0" apply false
+    id("org.jetbrains.kotlin.android") version "1.9.0" apply false
+}

+ 23 - 0
gradle.properties

@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true

+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Mon Dec 25 14:43:25 CST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 17 - 0
settings.gradle.kts

@@ -0,0 +1,17 @@
+pluginManagement {
+    repositories {
+        google()
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.name = "TinyPlayerAndroid"
+include(":app")