适配Android 4.4(普通电视机盒子)至Android10
build.gradle(app)
yaml
plugins {
alias(libs.plugins.android.application)
}
android {
namespace 'com.acplt.m3u8'
compileSdk 36
defaultConfig {
applicationId "com.acplt.m3u8"
minSdk 19
targetSdk 36
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true // 启用代码混淆以减小大小
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Multidex依赖
implementation 'androidx.multidex:multidex:2.0.1'
// 使用精简版的ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer:2.19.1'
// 或者只引入需要的模块(更推荐这种方式)
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1'
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation libs.navigation.fragment
implementation libs.navigation.ui
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}
AndroidManifest.xml
xml
<?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:name="androidx.multidex.MultiDexApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/handicon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/handicon"
android:supportsRtl="true"
android:theme="@style/Theme.M3U8"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:screenOrientation="landscape"
android:exported="false" />
<activity
android:name=".ListShow"
android:exported="true"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ListShow.java
java
package com.acplt.m3u8;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class ListShow extends Activity {
private GridView gridView;
private ProgressBar progressBar;
private VideoAdapter adapter;
private static final String BASE_URL = "http://192.168.0.155:8080/videos/";
List<String> json_url;
private String Get_LIST_URL()
{
return BASE_URL+json_url.get(json_url.size()-1);
}
//private static final String LIST_URL = BASE_URL + "list.json";
private void hideStatusBar() {
// 方法1: 使用 WindowManager 标志
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
// 方法2: 使用系统UI可见性 (API 16+)
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(uiOptions);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//实现全屏
getWindow().setDecorFitsSystemWindows(false);
//隐藏底部的navigation操作栏
getWindow().getInsetsController().hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
//将底部的navigation操作栏弄成透明,滑动显示,并且浮在上面
getWindow().getInsetsController().setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 如果当前版本小于HONEYCOMB版本,即3.0版本
requestWindowFeature(Window.FEATURE_NO_TITLE);
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
//让内容延伸进刘海
WindowManager.LayoutParams params = window.getAttributes();
//这里api是9.0以后的。所以这里需要添加版本判断
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
window.setAttributes(params);
//设置成沉浸式
int flags = /*View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | */View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
int visibility = window.getDecorView().getSystemUiVisibility();
visibility |= flags;
window.getDecorView().setSystemUiVisibility(visibility);
}
else {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
DisplayMetrics outMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getRealMetrics(outMetrics);
// 方法3: 隐藏状态栏和导航栏 (完全沉浸式)
// int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN
// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
// decorView.setSystemUiVisibility(uiOptions);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hideStatusBar();
setContentView(R.layout.activity_list_show);
json_url=new ArrayList<>();
json_url.add("list.json");
initViews();
loadVideoData();
}
private void initViews() {
gridView = findViewById(R.id.gridView);
progressBar = findViewById(R.id.progressBar);
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
VideoItem videoItem = (VideoItem) parent.getItemAtPosition(position);
if (videoItem != null) {
String videoUrl=videoItem.getU();
if(videoUrl.endsWith(".json"))
{
json_url.add(videoUrl);
loadVideoData();
return;
}
if(videoUrl.indexOf("http")!=0)
videoUrl = BASE_URL + videoUrl + ".m3u8";
MainActivity.currentUrl=videoUrl;
handler.sendEmptyMessage(0x4);
// Toast.makeText(ListShow.this,
// "视频链接: " + videoUrl,
// Toast.LENGTH_LONG).show();
}
}
});
}
String json_s;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
progressBar.setVisibility(View.GONE);
switch (msg.what) {
case 0x1: // 成功
List<VideoItem> videoList = (List<VideoItem>) msg.obj;
if(adapter!=null)
{
adapter.ReLoad(videoList);
}
else {
adapter = new VideoAdapter(ListShow.this, videoList);
}
gridView.setAdapter(adapter);
gridView.setVisibility(View.VISIBLE);
break;
case 0x2: // 错误
String error = (String) msg.obj;
Toast.makeText(ListShow.this, error, Toast.LENGTH_SHORT).show();
gridView.setVisibility(View.VISIBLE); // 显示空列表
break;
case 0x3:
if(json_s==null)
break;
parseAndDisplayDataWithJsonObject(json_s);
json_s=null;
break;
case 0x4:
Intent intent=new Intent();
intent.setClass(ListShow.this,MainActivity.class);
startActivity(intent);
break;
}
}
};
private void parseAndDisplayDataWithJsonObject(String jsonString) {
try {
JSONObject jsonObject = new JSONObject(jsonString);
String id = jsonObject.getString("id");
JSONArray dataArray = jsonObject.getJSONArray("data");
List<VideoItem> videoList = new ArrayList<>();
for (int i = 0; i < dataArray.length(); i++) {
JSONObject item = dataArray.getJSONObject(i);
VideoItem videoItem = new VideoItem();
videoItem.setN(item.getString("n"));
videoItem.setU(item.getString("u"));
videoItem.setG(item.getString("g"));
videoList.add(videoItem);
}
if(adapter==null)
{
adapter = new VideoAdapter(ListShow.this, videoList);
gridView.setAdapter(adapter);
}
else
{
adapter.ReLoad(videoList);
adapter.notifyDataSetChanged();
}
progressBar.setVisibility(View.GONE);
gridView.setVisibility(View.VISIBLE);
} catch (JSONException e) {
} catch (Exception e) {
}
}
private void loadVideoData() {
progressBar.setVisibility(View.VISIBLE);
gridView.setVisibility(View.GONE);
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(Get_LIST_URL());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
InputStream inputStream = connection.getInputStream();
ByteArrayOutputStream bt=new ByteArrayOutputStream();
byte[] t=new byte[256];
int lens=0;
while((lens=inputStream.read(t))>0)
{
bt.write(t,0,lens);
}
String line=new String(bt.toByteArray(),"utf8");
bt.reset();
bt.close();
json_s=line;
handler.sendEmptyMessage(0x3);
} else {
Message msg = handler.obtainMessage(0x2, "HTTP错误: " + responseCode);
handler.sendMessage(msg);
}
connection.disconnect();
} catch (Exception e) {
Message msg = handler.obtainMessage(0x2, "错误: " + e.getMessage());
handler.sendMessage(msg);
}
}
}).start();
}
@Override
protected void onDestroy() {
if(adapter!=null)
{
adapter.clearCache();
}
super.onDestroy();
}
@SuppressLint("GestureBackNavigation")
@Override
public void onBackPressed() {
if(json_url.size()<=1)
super.onBackPressed();
else
{
json_url.remove(json_url.size()-1);
loadVideoData();
}
}
}
MainActivity.java
java
package com.acplt.m3u8;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Timer;
import java.util.TimerTask;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
public class MainActivity extends Activity {
private SimpleExoPlayer player;
private PlayerView playerView;
private Button btnChangeUrl;
private LinearLayout customProgressLayout;
private SeekBar seekBar;
private TextView tvCurrentTime, tvTotalTime;
private Handler progressHandler = new Handler();
private boolean isProgressVisible = false;
public static String currentUrl = "http://192.168.0.155:8080/videos/a.m3u8";
public static Timer task;
public static Handler task_hand;
public static long min_time=0;
public static float tz_jd=0;
public static float tz_jd_bl=10;
public static int cl_ct=0;
TextView tz_text;
private void hideStatusBar() {
// 方法1: 使用 WindowManager 标志
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// 方法2: 使用系统UI可见性 (API 16+)
View decorView = getWindow().getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(uiOptions);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
//实现全屏
getWindow().setDecorFitsSystemWindows(false);
//隐藏底部的navigation操作栏
getWindow().getInsetsController().hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
//将底部的navigation操作栏弄成透明,滑动显示,并且浮在上面
getWindow().getInsetsController().setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 如果当前版本小于HONEYCOMB版本,即3.0版本
requestWindowFeature(Window.FEATURE_NO_TITLE);
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
//让内容延伸进刘海
WindowManager.LayoutParams params = window.getAttributes();
//这里api是9.0以后的。所以这里需要添加版本判断
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
window.setAttributes(params);
//设置成沉浸式
int flags = /*View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | */View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
int visibility = window.getDecorView().getSystemUiVisibility();
visibility |= flags;
window.getDecorView().setSystemUiVisibility(visibility);
}
else {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
DisplayMetrics outMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getRealMetrics(outMetrics);
// 方法3: 隐藏状态栏和导航栏 (完全沉浸式)
// int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN
// | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
// decorView.setSystemUiVisibility(uiOptions);
}
private void enableTls12ForKitKat() {
try {
SSLContext sc = SSLContext.getInstance("TLSv1.2");
sc.init(null, null, null);
// 设置默认的 SSLSocketFactory
SSLSocketFactory defaultSocketFactory = new Tls12SocketFactory(sc.getSocketFactory());
javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(defaultSocketFactory);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
Log.e("TLS", "启用 TLS 1.2 失败", e);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hideStatusBar();
setContentView(R.layout.activity_main);
task_hand=myhand;
min_time=0;
tz_jd_bl=10;
cl_ct=0;
tz_jd=0;
if(task==null)
{
task=new Timer();
task.schedule(new TimerTask() {
@Override
public void run() {
if(min_time<=0)return;
min_time-=100;
if(min_time<=0)
{
if(task_hand!=null)
{
tz_jd=tz_jd/100;
task_hand.sendEmptyMessage(0x10);
}
}
}
},100,100);
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
enableTls12ForKitKat();
}
initializeViews();
setupPlayer();
setupClickListeners();
setupProgressBar();
}
private void initializeViews() {
playerView = findViewById(R.id.playerView);
btnChangeUrl = findViewById(R.id.btnChangeUrl);
customProgressLayout = findViewById(R.id.customProgressLayout);
seekBar = findViewById(R.id.seekBar);
tvCurrentTime = findViewById(R.id.tvCurrentTime);
tvTotalTime = findViewById(R.id.tvTotalTime);
tz_text = findViewById(R.id.tz);
tz_text.setVisibility(View.INVISIBLE);
}
private void setupPlayer() {
// 创建播放器
player = new SimpleExoPlayer.Builder(this).build();
playerView.setPlayer(player);
// 设置自适应尺寸
playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
// 加载初始视频
loadVideo(currentUrl);
// 播放状态监听
player.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_READY) {
updateTotalTime();
}
}
});
}
private void setupClickListeners() {
// 更改URL按钮点击事件
btnChangeUrl.setOnClickListener(v -> showUrlInputDialog());
// 播放器区域点击事件 - 显示/隐藏进度条
playerView.setOnClickListener(v -> toggleProgressVisibility());
// 进度条拖动事件
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser && player != null) {
long duration = player.getDuration();
if (duration > 0) {
long newPosition = (duration * progress) / 100;
player.seekTo(newPosition);
updateCurrentTime();
}
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
progressHandler.removeCallbacks(updateProgressTask);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
startProgressUpdates();
}
});
}
private void setupProgressBar() {
startProgressUpdates();
}
private void loadVideo(String url) {
if (player != null) {
MediaItem mediaItem = MediaItem.fromUri(url);
player.setMediaItem(mediaItem);
player.prepare();
player.setPlayWhenReady(true);
}
}
boolean can_queding=false;
EditText etUrlInput;
Dialog dialog;
private void showUrlInputDialog() {
dialog = new Dialog(this);
dialog.setContentView(R.layout.dialog_url_input);
etUrlInput = dialog.findViewById(R.id.etUrlInput);
Button btnClose = dialog.findViewById(R.id.btnClose);
Button btnConfirm = dialog.findViewById(R.id.btnConfirm);
etUrlInput.setText("");
btnClose.setOnClickListener(v -> dialog.dismiss());
can_queding=true;
btnConfirm.setOnClickListener(v -> {
String newUrl = etUrlInput.getText().toString().trim();
if (!newUrl.isEmpty()) {
if(newUrl.indexOf("http")!=0)
newUrl="http://192.168.0.155:8080/videos/"+newUrl+".m3u8";
currentUrl = newUrl;
can_queding=false;
loadVideo(newUrl);
dialog.dismiss();
} else {
Toast.makeText(MainActivity.this, "请输入有效的URL", Toast.LENGTH_SHORT).show();
}
});
dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
can_queding=false;
etUrlInput=null;
}
});
dialog.show();
}
private void toggleProgressVisibility() {
if (isProgressVisible) {
customProgressLayout.setVisibility(View.GONE);
} else {
customProgressLayout.setVisibility(View.VISIBLE);
// 3秒后自动隐藏
progressHandler.postDelayed(() -> {
if (isProgressVisible) {
customProgressLayout.setVisibility(View.GONE);
isProgressVisible = false;
}
}, 3000);
}
isProgressVisible = !isProgressVisible;
}
private void startProgressUpdates() {
progressHandler.postDelayed(updateProgressTask, 1000);
}
private Runnable updateProgressTask = new Runnable() {
@Override
public void run() {
updateProgress();
progressHandler.postDelayed(this, 1000);
}
};
@SuppressLint("DefaultLocale")
private void updateProgress() {
if (player != null && player.isPlaying()) {
long duration = player.getDuration();
long position = player.getCurrentPosition();
if (duration > 0) {
int progress = (int) ((position * 100) / duration);
seekBar.setProgress(progress);
updateCurrentTime();
}
}
}
@SuppressLint("DefaultLocale")
private void updateCurrentTime() {
if (player != null) {
long position = player.getCurrentPosition();
long minutes = (position / 1000) / 60;
long seconds = (position / 1000) % 60;
tvCurrentTime.setText(String.format("%02d:%02d", minutes, seconds));
}
}
@SuppressLint("DefaultLocale")
private void updateTotalTime() {
if (player != null) {
long duration = player.getDuration();
if (duration > 0) {
long minutes = (duration / 1000) / 60;
long seconds = (duration / 1000) % 60;
tvTotalTime.setText(String.format("%02d:%02d", minutes, seconds));
}
}
}
@Override
protected void onPause() {
super.onPause();
if (player != null) {
player.setPlayWhenReady(false);
}
progressHandler.removeCallbacks(updateProgressTask);
}
@Override
protected void onResume() {
super.onResume();
if (player != null) {
player.setPlayWhenReady(true);
}
startProgressUpdates();
}
@Override
protected void onDestroy() {
if (player != null) {
player.release();
player = null;
}
task_hand=null;
min_time=0;
progressHandler.removeCallbacks(updateProgressTask);
super.onDestroy();
}
void set_bl(float bl)
{
if(player!= null)
{
long duration = (long)(player.getDuration()*bl);
player.seekTo(duration);
}
}
Handler myhand=new Handler()
{
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what)
{
case 0x1:
if (can_queding) {
String newUrl = etUrlInput.getText().toString().trim();
if (!newUrl.isEmpty()) {
can_queding = false;
if(newUrl.indexOf("http")!=0)
newUrl = "http://192.168.0.155:8080/videos/" + newUrl + ".m3u8";
currentUrl = newUrl;
loadVideo(newUrl);
dialog.dismiss();
} else {
Toast.makeText(MainActivity.this, "请输入有效的URL", Toast.LENGTH_SHORT).show();
}
}
else
{
showUrlInputDialog();
}
break;
case 0x2:
if(player!= null)
{
long duration = player.getDuration();
long position = player.getCurrentPosition();
position+=5000;
if(position>duration)
position=duration;
player.seekTo(position);
}
break;
case 0x3:
if(player!= null)
{
long position = player.getCurrentPosition();
position-=5000;
if(position<0)
position=0;
player.seekTo(position);
}
break;
case 0x20:
if(cl_ct>2)
tz_text.setText(String.format("%05.2f",tz_jd));
else
tz_text.setText(String.format("%02d",(int)tz_jd));
tz_text.setVisibility(View.VISIBLE);
break;
case 0x10:
set_bl(tz_jd);
tz_text.setVisibility(View.INVISIBLE);
tz_jd=0;
tz_jd_bl=10;
cl_ct=0;
break;
// case 0x11:
// set_bl(0.1f);
// break;
// case 0x12:
// set_bl(0.2f);
// break;
// case 0x13:
// set_bl(0.3f);
// break;
// case 0x14:
// set_bl(0.4f);
// break;
// case 0x15:
// set_bl(0.5f);
// break;
// case 0x16:
// set_bl(0.6f);
// break;
// case 0x17:
// set_bl(0.7f);
// break;
// case 0x18:
// set_bl(0.8f);
// break;
// case 0x19:
// set_bl(0.9f);
// break;
}
super.handleMessage(msg);
}
};
public void Tz(int dw)
{
if(cl_ct>=4)return;
min_time=1500;
tz_jd+=tz_jd_bl*dw;
tz_jd_bl/=10;
cl_ct+=1;
myhand.sendEmptyMessage(0x20);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_RIGHT:
myhand.sendEmptyMessage(0x2);
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
myhand.sendEmptyMessage(0x3);
break;
case KeyEvent.KEYCODE_0:
Tz(0);
break;
case KeyEvent.KEYCODE_1:
Tz(1);
break;
case KeyEvent.KEYCODE_2:
Tz(2);
break;
case KeyEvent.KEYCODE_3:
Tz(3);
break;
case KeyEvent.KEYCODE_4:
Tz(4);
break;
case KeyEvent.KEYCODE_5:
Tz(5);
break;
case KeyEvent.KEYCODE_6:
Tz(6);
break;
case KeyEvent.KEYCODE_7:
Tz(7);
break;
case KeyEvent.KEYCODE_8:
Tz(8);
break;
case KeyEvent.KEYCODE_9:
Tz(9);
break;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理触摸事件(包括鼠标)
myhand.sendEmptyMessage(0x1);
return false;
}
}
NetworkUtils.java
java
package com.acplt.m3u8;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class NetworkUtils {
public interface JsonResponseCallback {
void onSuccess(String jsonResponse);
void onError(String error);
}
public interface BitmapCallback {
void onSuccess(Bitmap bitmap);
void onError(String error);
}
public static void getJsonFromUrl(String urlString, JsonResponseCallback callback) {
new JsonRequestTask(callback).execute(urlString);
}
public static void getBitmapFromUrl(String urlString, BitmapCallback callback) {
new BitmapRequestTask(callback).execute(urlString);
}
private static class JsonRequestTask extends AsyncTask<String, Void, String> {
private JsonResponseCallback callback;
JsonRequestTask(JsonResponseCallback callback) {
this.callback = callback;
}
@Override
protected String doInBackground(String... params) {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL url = new URL(params[0]);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.setRequestProperty("Connection", "close");
int responseCode = connection.getResponseCode();
Log.d("NetworkUtils", "Response Code: " + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK) {
InputStream inputStream = connection.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} else {
return "Error: HTTP " + responseCode;
}
} catch (Exception e) {
Log.e("NetworkUtils", "Error fetching JSON: " + e.getMessage(), e);
return "Error: " + e.getMessage();
} finally {
try {
if (reader != null) reader.close();
if (connection != null) connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPostExecute(String result) {
if (callback != null) {
if (result != null && !result.startsWith("Error:")) {
callback.onSuccess(result);
} else {
callback.onError(result != null ? result : "Unknown error");
}
}
}
}
private static class BitmapRequestTask extends AsyncTask<String, Void, Bitmap> {
private BitmapCallback callback;
BitmapRequestTask(BitmapCallback callback) {
this.callback = callback;
}
@Override
protected Bitmap doInBackground(String... params) {
HttpURLConnection connection = null;
InputStream input = null;
try {
URL url = new URL(params[0]);
connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
input = connection.getInputStream();
return BitmapFactory.decodeStream(input);
}
return null;
} catch (Exception e) {
Log.e("NetworkUtils", "Error fetching bitmap: " + e.getMessage(), e);
return null;
} finally {
try {
if (input != null) input.close();
if (connection != null) connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPostExecute(Bitmap result) {
if (callback != null) {
if (result != null) {
callback.onSuccess(result);
} else {
callback.onError("Failed to load image");
}
}
}
}
}
StrokeTextView.java
java
package com.acplt.m3u8;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextPaint;
import android.util.AttributeSet;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
public class StrokeTextView extends androidx.appcompat.widget.AppCompatTextView {
private int strokeColor = Color.BLACK;
private float strokeWidth = 0;
public StrokeTextView(Context context) {
super(context);
}
public StrokeTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public StrokeTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StrokeTextView);
strokeColor = a.getColor(R.styleable.StrokeTextView_strokeColor, Color.BLACK);
strokeWidth = a.getDimension(R.styleable.StrokeTextView_strokeWidth, 0);
a.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
final ColorStateList textColor = getTextColors();
// 先绘制描边
getPaint().setStyle(Paint.Style.STROKE);
getPaint().setStrokeWidth(strokeWidth);
setTextColor(strokeColor);
super.onDraw(canvas);
// 再绘制填充文字
getPaint().setStyle(Paint.Style.FILL);
getPaint().setStrokeWidth(0);
setTextColor(textColor);
super.onDraw(canvas);
}
public void setStrokeColor(int color) {
strokeColor = color;
invalidate();
}
public void setStrokeWidth(float width) {
strokeWidth = width;
invalidate();
}
}
Tls12SocketFactory.java
java
package com.acplt.m3u8;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Tls12SocketFactory extends SSLSocketFactory {
private static final String[] TLS_V12_SUPPORTED = {"TLSv1.2", "TLSv1.1", "TLSv1"};
private final SSLSocketFactory delegate;
public Tls12SocketFactory(SSLSocketFactory base) {
this.delegate = base;
}
@Override
public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTlsOnSocket(delegate.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return enableTlsOnSocket(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return enableTlsOnSocket(delegate.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTlsOnSocket(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTlsOnSocket(delegate.createSocket(address, port, localAddress, localPort));
}
private Socket enableTlsOnSocket(Socket socket) {
if (socket instanceof SSLSocket) {
SSLSocket sslSocket = (SSLSocket) socket;
// 获取支持的协议并添加 TLS 1.2
String[] supportedProtocols = sslSocket.getSupportedProtocols();
List<String> enabledProtocols = new ArrayList<>();
for (String protocol : TLS_V12_SUPPORTED) {
for (String supported : supportedProtocols) {
if (protocol.equals(supported)) {
enabledProtocols.add(protocol);
break;
}
}
}
if (!enabledProtocols.isEmpty()) {
sslSocket.setEnabledProtocols(enabledProtocols.toArray(new String[0]));
}
}
return socket;
}
}
VideoAdapter.java
java
package com.acplt.m3u8;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
public class VideoAdapter extends BaseAdapter {
private Context context;
private List<VideoItem> videoList;
private LayoutInflater inflater;
// 添加内存缓存
private LruCache<String, Bitmap> memoryCache;
public void ReLoad(List<VideoItem> videoList)
{
this.videoList = videoList;
// clearCache();
// initMemoryCache();
}
public VideoAdapter(Context context, List<VideoItem> videoList) {
this.context = context;
this.videoList = videoList;
this.inflater = LayoutInflater.from(context);
// 初始化内存缓存
initMemoryCache();
}
private void initMemoryCache() {
// 获取最大可用内存的1/8作为缓存大小
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
memoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 返回Bitmap占用的内存大小,单位KB
return bitmap.getByteCount() / 1024;
}
};
}
@Override
public int getCount() {
return videoList != null ? videoList.size() : 0;
}
@Override
public Object getItem(int position) {
return videoList != null ? videoList.get(position) : null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.grid_item, parent, false);
holder = new ViewHolder();
holder.videoThumbnail = convertView.findViewById(R.id.videoThumbnail);
holder.videoTitle = convertView.findViewById(R.id.videoTitle);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
VideoItem videoItem = videoList.get(position);
holder.videoTitle.setText(videoItem.getN());
// 加载图片
String imageUrl = "http://192.168.0.155:8080/videos/image/" + videoItem.getG();
loadImage(imageUrl, holder.videoThumbnail);
return convertView;
}
private void loadImage(String imageUrl, ImageView imageView) {
// 设置tag,防止图片错乱
imageView.setTag(imageUrl);
// 先从内存缓存中获取
Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
if (bitmap != null) {
// 如果缓存中有,直接使用
imageView.setImageBitmap(bitmap);
} else {
// 缓存中没有,从网络加载
NetworkUtils.getBitmapFromUrl(imageUrl, new NetworkUtils.BitmapCallback() {
@Override
public void onSuccess(Bitmap bitmap) {
// 检查ImageView的tag是否还是这个URL,防止复用导致的错乱
if (imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(bitmap);
}
// 将图片加入缓存
addBitmapToMemoryCache(imageUrl, bitmap);
}
@Override
public void onError(String error) {
if (imageView.getTag().equals(imageUrl)) {
imageView.setImageResource(android.R.drawable.ic_media_play);
}
}
});
}
}
// 添加图片到内存缓存
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null && bitmap != null) {
memoryCache.put(key, bitmap);
}
}
// 从内存缓存获取图片
private Bitmap getBitmapFromMemoryCache(String key) {
return memoryCache.get(key);
}
private static class ViewHolder {
ImageView videoThumbnail;
TextView videoTitle;
}
// 清理缓存的方法
public void clearCache() {
if (memoryCache != null) {
memoryCache.evictAll();
}
}
}
VideoItem.java
java
package com.acplt.m3u8;
public class VideoItem {
private String n; // 视频名称
private String u; // 视频链接
private String g; // 展示图片
public VideoItem() {}
public VideoItem(String n, String u, String g) {
this.n = n;
this.u = u;
this.g = g;
}
public String getN() {
return n;
}
public void setN(String n) {
this.n = n;
}
public String getU() {
return u;
}
public void setU(String u) {
this.u = u;
}
public String getG() {
return g;
}
public void setG(String g) {
this.g = g;
}
}
VideoResponse.java
java
package com.acplt.m3u8;
import java.util.List;
public class VideoResponse {
private String id;
private List<VideoItem> data;
public VideoResponse() {}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<VideoItem> getData() {
return data;
}
public void setData(List<VideoItem> data) {
this.data = data;
}
}
activity_list_show.xml
xml
<?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="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="我的视频"
android:textSize="24sp"
android:textStyle="bold"
android:gravity="center"
android:padding="16dp"
android:background="#2196F3"
android:textColor="#FFFFFF" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp" />
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:numColumns="auto_fit"
android:columnWidth="140dp"
android:horizontalSpacing="8dp"
android:verticalSpacing="8dp"
android:stretchMode="columnWidth"
android:gravity="center"
android:padding="8dp" />
</LinearLayout>
activity_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- 播放器视图 -->
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:resize_mode="fit"
app:show_buffering="when_playing"
app:use_controller="true" />
<!-- 顶部控制栏 -->
<Button
android:id="@+id/btnChangeUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更改URL"
android:textColor="#00FFFFFF"
android:background="#00FFFFFF"
android:backgroundTint="#4CAF50" />
<com.acplt.m3u8.StrokeTextView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:text="00.00"
android:textColor="#FFF"
android:background="#06EEEEEE"
android:textSize="60dp"
android:layout_margin="20dp"
android:id="@+id/tz"
android:padding="20dp"
android:gravity="center"
app:strokeColor="#000000"
app:strokeWidth="2dp"
android:layout_alignParentRight="true"
/>
<!-- 自定义进度条 -->
<LinearLayout
android:id="@+id/customProgressLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#80000000"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone">
<SeekBar
android:id="@+id/seekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progress="0" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tvCurrentTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="14sp" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvTotalTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
dialog_url_input.xml
xml
<?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="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="输入M3U8 URL"
android:textSize="18sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="16dp" />
<EditText
android:id="@+id/etUrlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入M3U8视频地址"
android:inputType="textUri"
android:text="http://192.168.0.155:8080/videos/a.m3u8" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="20dp"
android:gravity="end">
<Button
android:id="@+id/btnClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关闭"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnConfirm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="确定"
android:backgroundTint="#4CAF50"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
grid_item.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="140dp"
android:layout_height="247dp"
android:orientation="vertical"
android:background="#0000"
android:padding="10dp"
android:layout_margin="8dp"
>
<LinearLayout
android:layout_width="120dp"
android:layout_height="227dp"
android:orientation="vertical"
android:background="@drawable/item_background"
>
<ImageView
android:id="@+id/videoThumbnail"
android:layout_width="100dp"
android:layout_height="157dp"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:scaleType="centerCrop"
android:src="@drawable/placeholder" />
<TextView
android:id="@+id/videoTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="视频标题"
android:textSize="12sp"
android:textColor="#333333"
android:maxLines="2"
android:ellipsize="end"
android:gravity="center" />
</LinearLayout>
</LinearLayout>
item_background.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<stroke android:width="1dp" android:color="#E0E0E0" />
<corners android:radius="8dp" />
</shape>
network_security_config.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>