Initial hlsmplayer application

This commit is contained in:
root
2026-09-27 17:13:02 +08:00
commit be5a40bfe3
1214 changed files with 315753 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
plugins { id("com.android.application") }
android {
namespace = "com.hlsmplayer"
compileSdk = 36
defaultConfig {
applicationId = "com.hlsmplayer"
minSdk = 23
targetSdk = 34
versionCode = 1011001300
versionName = "1.11.1"
}
buildTypes {
getByName("release") {
isShrinkResources = true
isMinifyEnabled = true
proguardFiles("proguard-rules.txt", getDefaultProguardFile("proguard-android-optimize.txt"))
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation(project(":lib-exoplayer"))
implementation(project(":lib-exoplayer-hls"))
implementation(project(":lib-ui"))
testImplementation(libs.junit)
}
+1
View File
@@ -0,0 +1 @@
# Proguard rules specific to hlsmplayer.
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
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.
-->
<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"
tools:node="remove"/>
<uses-permission
android:name="android.permission.WAKE_LOCK"
tools:node="remove"/>
<uses-feature
android:name="android.software.leanback"
android:required="false"/>
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
<application
android:label="@string/application_name"
android:icon="@mipmap/ic_launcher"
android:banner="@drawable/tv_banner"
android:allowBackup="false"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
tools:targetApi="29">
<activity android:name=".PlayerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:label="@string/application_name"
android:theme="@style/PlayerTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,77 @@
/*
* Copyright 2026 The Android Open Source Project
*
* 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.
*/
package com.hlsmplayer;
import java.net.URI;
import java.net.URISyntaxException;
/** Validates and resolves the relative endpoints published by the VOD catalog. */
final class MediaCatalogUrl {
private static final String PLAYLIST_PREFIX = "/hls/";
private static final String COVER_PREFIX = "/api/media/covers/";
private MediaCatalogUrl() {}
public static boolean isValidPlaylistUrl(String value) {
return isValidRelativePath(value, PLAYLIST_PREFIX) && value.endsWith("/master.m3u8");
}
public static boolean isValidCoverUrl(String value) {
return isValidRelativePath(value, COVER_PREFIX) && value.endsWith(".jpg");
}
public static String resolve(String serverBase, String relativePath) {
if (serverBase == null || relativePath == null) {
throw new IllegalArgumentException("Missing server base or relative path");
}
try {
URI base = new URI(serverBase + "/");
URI endpoint = new URI(relativePath);
return base.resolve(endpoint).toASCIIString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid server endpoint", e);
}
}
private static boolean isValidRelativePath(String value, String requiredPrefix) {
if (value == null || !value.startsWith(requiredPrefix)) {
return false;
}
try {
URI uri = new URI(value);
String rawPath = uri.getRawPath();
return !uri.isAbsolute()
&& uri.getRawAuthority() == null
&& uri.getRawQuery() == null
&& uri.getRawFragment() == null
&& rawPath != null
&& rawPath.equals(value)
&& !containsDotSegment(rawPath);
} catch (URISyntaxException e) {
return false;
}
}
private static boolean containsDotSegment(String path) {
for (String segment : path.split("/")) {
if (".".equals(segment) || "..".equals(segment)) {
return true;
}
}
return false;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape>
<solid android:color="#BDF04438"/>
</shape>
</item>
<item android:state_selected="true">
<shape>
<solid android:color="#804B5563"/>
</shape>
</item>
<item>
<shape>
<solid android:color="@android:color/transparent"/>
</shape>
</item>
</selector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#4B5563" />
<stroke android:width="1dp" android:color="#66798A9D" />
</shape>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#F04438"/>
</shape>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="320dp"
android:height="180dp"
android:viewportWidth="320"
android:viewportHeight="180">
<path
android:fillColor="#101214"
android:pathData="M0,0H320V180H0Z" />
<path
android:fillColor="#F04438"
android:pathData="M126,48L126,132L202,90Z" />
<path
android:fillColor="#F5F7FA"
android:fillAlpha="0.88"
android:pathData="M34,138H286V146H34Z" />
</vector>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/media_browser_item_selector"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/browser_item_cover"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@null"
android:scaleType="centerCrop"
android:src="@drawable/media_cover_placeholder" />
<TextView
android:id="@+id/browser_item_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#F5F7FA"
android:textSize="18sp" />
</LinearLayout>
+128
View File
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2026 The Android Open Source Project
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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#101214"
android:keepScreenOn="true">
<GridLayout
android:id="@+id/player_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnCount="2"
android:rowCount="2">
<include
android:id="@+id/player_pane_0"
layout="@layout/player_pane"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_columnWeight="1"
android:layout_rowWeight="1" />
<include
android:id="@+id/player_pane_1"
layout="@layout/player_pane"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_columnWeight="1"
android:layout_rowWeight="1" />
<include
android:id="@+id/player_pane_2"
layout="@layout/player_pane"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_columnWeight="1"
android:layout_rowWeight="1" />
<include
android:id="@+id/player_pane_3"
layout="@layout/player_pane"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_columnWeight="1"
android:layout_rowWeight="1" />
</GridLayout>
<FrameLayout
android:id="@+id/media_browser_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#40101214"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingHorizontal="28dp"
android:paddingVertical="24dp">
<TextView
android:id="@+id/browser_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="start"
android:maxLines="1"
android:textColor="#F5F7FA"
android:textSize="20sp" />
<TextView
android:id="@+id/browser_message"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:textColor="#F5F7FA"
android:textSize="18sp"
android:visibility="gone" />
<ListView
android:id="@+id/browser_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:divider="#664B5563"
android:dividerHeight="1dp"
android:drawSelectorOnTop="true"
android:scrollbars="none" />
<Button
android:id="@+id/browser_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:text="@string/retry"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/browser_cover_preview"
android:layout_width="1dp"
android:layout_height="1dp"
android:layout_gravity="top|end"
android:contentDescription="@string/media_cover_preview"
android:scaleType="centerCrop"
android:visibility="gone" />
</FrameLayout>
</FrameLayout>
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<androidx.media3.ui.PlayerView
android:id="@+id/pane_player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false" />
<TextView
android:id="@+id/pane_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:ellipsize="end"
android:maxLines="1"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
android:textColor="#FFFFFFFF"
android:textSize="16sp"
android:visibility="gone" />
<View
android:id="@+id/pane_focus_indicator"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_gravity="top|end"
android:layout_margin="12dp"
android:background="@drawable/player_focus_indicator"
android:contentDescription="@string/player_focused"
android:visibility="gone" />
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/pane_time_bar"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_gravity="bottom"
android:focusable="false"
app:bar_gravity="bottom"
app:bar_height="3dp"
app:buffered_color="#99FFFFFF"
app:played_color="#F04438"
app:scrubber_color="#F04438"
app:scrubber_disabled_size="0dp"
app:scrubber_dragged_size="0dp"
app:scrubber_enabled_size="0dp"
app:touch_target_height="20dp"
app:unplayed_color="#66FFFFFF" />
</FrameLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 The Android Open Source Project
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.
-->
<resources>
<string name="application_name">hlsmplayer</string>
<string name="player_focused">播放器已获得焦点</string>
<string name="media_browser_title">媒体列表</string>
<string name="media_loading">正在加载媒体列表</string>
<string name="media_building">媒体目录正在生成,请稍后重试。</string>
<string name="media_load_error">无法加载媒体列表。</string>
<string name="media_empty">媒体列表为空。</string>
<string name="retry">重试</string>
<string name="media_cover_preview">视频封面预览</string>
<string name="server_settings">媒体服务器</string>
<string name="server_host">IP 地址或主机名</string>
<string name="server_port">端口</string>
<string name="server_save">保存</string>
<string name="server_cancel">取消</string>
<string name="server_invalid">请输入有效的 IP 地址或主机名,以及 1 到 65535 的端口。</string>
</resources>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 The Android Open Source Project
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.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PlayerTheme" parent="android:style/Theme.Material.NoActionBar">
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>
@@ -0,0 +1,59 @@
/*
* Copyright 2026 The Android Open Source Project
*
* 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.
*/
package com.hlsmplayer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class MediaCatalogUrlTest {
@Test
public void playlistUrl_acceptsEncodedCatalogUrl() {
String playlistUrl = "/hls/%E4%B8%AD%E6%96%87%20media/clip%20%231.mp4/master.m3u8";
assertTrue(MediaCatalogUrl.isValidPlaylistUrl(playlistUrl));
assertEquals(
"http://192.168.1.20:8124" + playlistUrl,
MediaCatalogUrl.resolve("http://192.168.1.20:8124", playlistUrl));
}
@Test
public void playlistUrl_rejectsOtherOriginsQueriesAndTraversal() {
assertFalse(MediaCatalogUrl.isValidPlaylistUrl("https://other.example/hls/clip.mp4/master.m3u8"));
assertFalse(MediaCatalogUrl.isValidPlaylistUrl("/hls/clip.mp4/master.m3u8?token=value"));
assertFalse(MediaCatalogUrl.isValidPlaylistUrl("/hls/../secret/master.m3u8"));
}
@Test
public void coverUrl_acceptsPublishedCachePath() {
String coverUrl = "/api/media/covers/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef.jpg";
assertTrue(MediaCatalogUrl.isValidCoverUrl(coverUrl));
assertEquals(
"http://vod.example:8124" + coverUrl,
MediaCatalogUrl.resolve("http://vod.example:8124", coverUrl));
}
@Test
public void coverUrl_rejectsLegacyAndMalformedPaths() {
assertFalse(MediaCatalogUrl.isValidCoverUrl("/api/media/cover?path=movies/clip.mp4"));
assertFalse(MediaCatalogUrl.isValidCoverUrl("/api/media/covers/cover.jpg#preview"));
assertFalse(MediaCatalogUrl.isValidCoverUrl("/api/media/covers/../cover.jpg"));
}
}