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>
相关推荐
茶憶16 小时前
UniApp 安卓端实现文件的生成,写入,获取文件大小以及压缩功能
android·javascript·vue.js·uni-app
2501_9159214316 小时前
uni-app 的 iOS 打包与上架流程,多工具协作
android·ios·小程序·uni-app·cocoa·iphone·webview
Lei活在当下1 天前
【Perfetto从入门到精通】4.使用 heapprofd 工具采样追踪 Java/Native 内存分配
android·性能优化·架构
alexhilton1 天前
学会在Jetpack Compose中加载Lottie动画资源
android·kotlin·android jetpack
summerkissyou19871 天前
Android-Camera-为啥不移到packages/module
android·相机
liang_jy1 天前
Android UID
android·面试
好游科技1 天前
语聊APP新生态!一站式语聊房语音直播APP源码开发搭建
音视频·webrtc·im即时通讯·社交软件·社交语音视频软件
nono牛1 天前
安卓/MTK平台日志关键词详解
android
TimeFine1 天前
Android AI解放生产力(四)实战:解放绘制UI的繁琐工作
android
sheji34161 天前
【开题答辩全过程】以 基于Android的社区车位共享管理系统的设计与实现为例,包含答辩的问题和答案
android