Android M3U8视频播放器

适配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>
相关推荐
音视频牛哥40 分钟前
轻量级RTSP服务的工程化设计与应用:从移动端到边缘设备的实时媒体架构
人工智能·计算机视觉·音视频·音视频开发·rtsp播放器·安卓rtsp服务器·安卓实现ipc功能
❀͜͡傀儡师43 分钟前
Docker部署视频下载器
docker·容器·音视频
q***57741 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober2 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿2 小时前
关于ObjectAnimator
android
zhangphil3 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我4 小时前
从头写一个自己的app
android·前端·flutter
EasyDSS4 小时前
视频推流平台EasyDSS无人机推流技术打造大型安保巡逻监控新模式
音视频·无人机
2501_907136825 小时前
开源视频批量处理工具FFmpeg Batch AV Converter
ffmpeg·音视频·软件需求