Android 使用Service服务,状态栏更新Notification通知,多线程下载m3u8视频(png图片式m3u8破解、图片伪ts破解、解密合并ts)

        开发自制浏览器的过程中,学习使用了Service服务和Notification通知,下载m3u8格式视频文件,记录分享给小伙伴们。

一、依赖配置

        开发用到的Android Sdk版本为32,额外用到的依赖主要是一个解密包 bcprov-jdk16-139.jar,本地引用的,放置于Android项目app目录下新建的libs文件夹中,jar包下载地址传送补充百度网盘下载​​​​​​​,build.gradle(app目录下)中引用方式如下

dependencies {
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    implementation 'org.apache.commons:commons-lang3:3.7'
    implementation files('libs\\bcprov-jdk16-139.jar')
}

 二、清单文件配置,设置权限,配置Service

        需注意,高版本网络访问和文件读写得在application标签属性中配置这俩usesCleartextTraffic="true" 、 requestLegacyExternalStorage="true"

<?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" /> <!-- 下载时 安卓6.0以下的直接将读写权限写入AndroidManifest.xml之中 -->
    <uses-permission
        android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!--通知权限、-->
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <!--存储权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:hardwareAccelerated="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/NoTitle"
        android:requestLegacyExternalStorage="true"
        android:usesCleartextTraffic="true"
        tools:targetApi="31">
        <service
            android:name=".service.DownloadService"
            android:enabled="true"
            android:exported="true"></service>

        <activity
            android:name=".activity.MainActivity"
            android:exported="true"
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
        <activity
            android:name=".activity.BrowserActivity"
            android:configChanges="orientation|screenSize"
            android:exported="false"
            android:hardwareAccelerated="true"
            android:launchMode="singleTop">
            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

三、导航栏通知,用到的视图相关xml文件

        progress_bar.xml,一般放置于drawable目录下

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <!-- 进度条背景色 -->
    <item android:id="@android:id/background">
        <shape>
            <corners android:radius="5dip" />
            <gradient
                android:startColor="#D8D8D8"
                android:centerColor="#D8D8D8"
                android:centerY="0.75"
                android:endColor="#D8D8D8"
                android:angle="270"
                />
        </shape>
    </item>

    <!-- 第二进度条 -->
    <item android:id="@android:id/secondaryProgress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:startColor="#b9a4ff"
                    android:centerColor="#c6b7ff"
                    android:centerY="0.75"
                    android:endColor="#c3b2ff"
                    android:angle="270"
                    />
            </shape>
        </clip>
    </item>

    <!-- 进度条 -->
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="5dip" />
                <gradient
                    android:startColor="#AEAEAE"
                    android:centerColor="#AEAEAE"
                    android:centerY="0.75"
                    android:endColor="#AEAEAE"
                    android:angle="270"
                    />
            </shape>
        </clip>
    </item>
</layer-list>

        notification_download.xml,一般放置于layout目录下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/colorLightGray1">
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center">
        <ImageView
            android:id="@+id/ivLogo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@color/colorLightGray1"
            android:layout_margin="10dip" />
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:layout_weight="1"
            android:layout_marginRight="14dip">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/gray2"
                android:text="自制浏览器下载任务"
                android:textSize="14dip"
                android:textStyle="bold"/>
            <ProgressBar
                android:id="@+id/pbDownload"
                android:layout_width="fill_parent"
                android:layout_height="6dip"
                android:layout_marginTop="6dip"
                android:layout_marginBottom="3dip"
                android:progress="0"
                android:max="100"
                android:progressDrawable="@drawable/progress_bar"
                style="?android:attr/progressBarStyleHorizontal"  />
            <TextView
                android:id="@+id/tvProcess"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/gray2"
                android:text="已下载0%"
                android:textSize="12dip"
                android:textStyle="bold" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

四、Service服务、Notification消息通知的使用

package cn.cheng.simpleBrower.service;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.service.notification.StatusBarNotification;
import android.widget.RemoteViews;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;

import java.io.File;
import java.util.regex.Matcher;

import cn.cheng.simpleBrower.R;
import cn.cheng.simpleBrower.activity.BrowserActivity;
import cn.cheng.simpleBrower.activity.MainActivity;
import cn.cheng.simpleBrower.custom.M3u8DownLoader;

/**
 * 下载Service (目前仅用于下载m3u8)
 */
public class DownloadService extends Service {

    // 通知管理器
    private NotificationManager nm;
    // 消息通知
    private Notification notification;
    // 线程处理工具
    private MyHandler myHandler;
    // 下载进度
    private int download_precent = 0;
    // 通知提示视图
    private RemoteViews views;
    // 通知id 每次下载通知要不一样
    private int notificationId = 1234;
    // 频道id 每次下载通知要不一样
    private String CHANNEL_ID = "一口一个大胖子,喜喜";

    public DownloadService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

        String title = intent.getStringExtra("title");
        if (title != null && !"".equals(title)) {
            CHANNEL_ID = title;
        }
        // 高版本通知Notification 必须先定义NotificationChannel
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID
                    , "name", NotificationManager.IMPORTANCE_DEFAULT);
            if (nm.getActiveNotifications().length > 0) {
                StatusBarNotification activeNotification = nm.getActiveNotifications()[nm.getActiveNotifications().length-1];
                if (activeNotification.getNotification() != null) {
                   String channelId = activeNotification.getNotification().getChannelId();
                   // System.out.println("5555555555555555555555555" + channelId);
                   // System.out.println("5555555555555555555555555" + CHANNEL_ID);
                   if (CHANNEL_ID.equals(channelId)) {
                       return super.onStartCommand(intent, flags, startId);
                   } else {
                       notificationId ++;
                   }
                   // System.out.println("**************************" + notificationId);
                }
            }
            nm.createNotificationChannel(channel);
        }

        // 创建一个Notification对象
        NotificationCompat.Builder nBuilder = new NotificationCompat.Builder(this, CHANNEL_ID);
        // 设置打开该通知,该通知自动消失
        nBuilder.setAutoCancel(false);
        // 设置通知的图标
        nBuilder.setSmallIcon(R.drawable.btn_like);
        // 设置通知内容的标题
        // nBuilder.setContentTitle("下载中");
        // 设置通知内容
        // nBuilder.setContentText("打开APP查看详情!");
        // 设置使用系统默认的声音、默认震动
        nBuilder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
        // 设置发送时间
        nBuilder.setWhen(System.currentTimeMillis());

        // 创建一个启动其他Activity的Intent
        Intent i = new Intent(this, BrowserActivity.class);
        String that = intent.getStringExtra("this");
        if ("MainActivity".equals(that)) {
            i = new Intent(this, MainActivity.class);
        }
        PendingIntent pendingIntent;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE);
        } else {
            pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_ONE_SHOT);
        }
        // 设置通知栏点击跳转
        nBuilder.setContentIntent(pendingIntent);

        // 创建通知
        notification = nBuilder.build();
        //设置任务栏中下载进程显示的views
        views = new RemoteViews(getPackageName(), R.layout.notification_download);
        notification.contentView = views;
        //将下载任务添加到任务栏中
        nm.notify(notificationId, notification);

        // 线程消息传递处理
        myHandler = new MyHandler(Looper.myLooper(), this);

        //初始化下载任务内容views
        String[] arr = new String[]{"0", notificationId+""};
        Message message = myHandler.obtainMessage(3, arr);
        myHandler.sendMessage(message);

        //启动线程开始执行下载任务
        if (Build.VERSION.SDK_INT >= 29) { // android 12的sd卡读写
            //启动线程开始执行下载任务
            String url = intent.getStringExtra("url");
            String dirName = intent.getStringExtra("dirName");
            if (dirName == null || "".equals(dirName)) {
                dirName = System.currentTimeMillis() + "";
            }
            if (title == null || "".equals(title)) {
                title = "test";
            }
            // M3u8DownLoader.test(url, myHandler);
            M3u8DownLoader m3u8Download =  new M3u8DownLoader(url, notificationId);
            //设置生成目录
            String dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
            m3u8Download.setDir(dir + "/SimpleBrower/" + dirName);
            //设置视频名称
            m3u8Download.setFileName(title);
            //设置线程数
            m3u8Download.setThreadCount(100);
            //设置重试次数
            m3u8Download.setRetryCount(8);
            //设置连接超时时间(单位:毫秒)
            m3u8Download.setTimeoutMillisecond(10000L);
            m3u8Download.start(myHandler);
        }
        return super.onStartCommand(intent, flags, startId);
    }

    // 线程消息传递处理
    private class MyHandler extends Handler {

        private Context context;

        public MyHandler(Looper looper, Context context) {
            super(looper);
            this.context = context;
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if (!(msg.obj instanceof String[])) {
                return;
            }
            String[] arr = (String[]) msg.obj;
            if (arr.length == 2) {
                switch (msg.what) {
                    case 0:
                        Toast.makeText(context, arr[0], Toast.LENGTH_SHORT).show();
                    case 1:
                        break;
                    case 2:
                        //下载完成后清除所有下载信息,执行安装提示
                        download_precent = 0;
                        nm.cancel(Integer.parseInt(arr[1]));
                        Toast.makeText(context, arr[0], Toast.LENGTH_SHORT).show();
                        //停止掉当前的服务
                        stopSelf();
                        break;
                    case 3:
                        String str = arr[0];
                        if (str.matches("^(([1-9]\\d*)(\\.\\d+)?)$|^((0)(\\.\\d+)?)$")) { // 判断数字
                            download_precent = (int) Float.parseFloat(str);
                            str = "已下载" + str + "%";
                        }
                        //更新状态栏上的下载进度信息
                        views.setTextViewText(R.id.tvProcess, str);
                        views.setProgressBar(R.id.pbDownload, 100, download_precent, false);
                        notification.contentView = views;
                        nm.notify(Integer.parseInt(arr[1]), notification);
                        break;
                    case 4:
                        nm.cancel(Integer.parseInt(arr[1]));
                        break;
                }
            }
        }
    }
}

五、重头戏当然是m3u8格式的文件下载

        其中ts解密主要针对AES,其他加密方式可按相应情况进行解密扩展;对于png等图片式m3u8文件,也就是伪ts文件的处理,查看其他文章学习到,可以观察其文件流对应16进制字符串,发现伪ts文件虽然有一些干扰的文件头,但实际的ts文件也隐藏其中,开始的字节片段和正常的ts文件有相同之处,转成16进制,其标志为FF4740,这样就能计算出伪ts文件实际有效字节开始之处,通过RandomAccessFile,seek设置这个字节数位置即可。

package cn.cheng.simpleBrower.custom;

import android.os.Environment;
import android.os.Handler;
import android.os.Message;

import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * m3u8格式视频下载器
 */
public class M3u8DownLoader {

    /**
     *
     * 解决java不支持AES/CBC/PKCS7Padding模式解密
     *
     */
    static {
        Security.addProvider(new BouncyCastleProvider());
    }


    //要下载的m3u8链接
    private final String DOWNLOADURL;

    // 通知id
    private final int id;

    // 获取伪png这种ts文件实际字节开始下标
    static int num = 0;

    //线程数
    private int threadCount = 1;

    //重试次数
    private int retryCount = 30;

    //链接连接超时时间(单位:毫秒)
    private long timeoutMillisecond = 1000L;

    //合并后的文件存储目录
    private String dir;

    //合并后的视频文件名称
    private String fileName;

    //已完成ts片段个数
    private int finishedCount = 0;

    //解密算法名称
    private String method;

    //密钥
    private String key = "";

    //所有ts片段下载链接
    private Set<String> tsSet = new LinkedHashSet<>();

    //解密后的片段
    private Set<File> finishedFiles = new ConcurrentSkipListSet<>(Comparator.comparingInt(o -> Integer.parseInt(o.getName().replace(".xyz", ""))));

    //已经下载的文件大小
    private BigDecimal downloadBytes = new BigDecimal(0);

    public M3u8DownLoader(String m3U8URL, int notificationId) {
        DOWNLOADURL = m3U8URL;
        id = notificationId;
    }

    public void setThreadCount(int threadCount) {
        this.threadCount = threadCount;
    }

    public void setRetryCount(int retryCount) {
        this.retryCount = retryCount;
    }

    public void setTimeoutMillisecond(long timeoutMillisecond) {
        this.timeoutMillisecond = timeoutMillisecond;
    }

    public void setDir(String dir) {
        this.dir = dir;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    /**
     * 模拟http请求获取内容
     *
     * @param urls http链接
     * @return 内容
     */
    private StringBuilder getUrlContent(String urls) throws M3u8Exception {
        int count = 1;
        HttpURLConnection httpURLConnection = null;
        StringBuilder content = new StringBuilder();
        while (count <= retryCount) {
            try {
                URL url = new URL(urls);
                httpURLConnection = (HttpURLConnection) url.openConnection();
                httpURLConnection.setConnectTimeout((int) timeoutMillisecond);
                httpURLConnection.setReadTimeout((int) timeoutMillisecond);
                httpURLConnection.setUseCaches(false);
                httpURLConnection.setDoInput(true);
                // 模拟电脑请求
                httpURLConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36");
                // 跨域设置相关
                httpURLConnection.setRequestProperty("Access-Control-Allow-Origin", "*");
                //* 代办允许所有方法
                httpURLConnection.setRequestProperty("Access-Control-Allow-Methods", "*");
                // Access-Control-Max-Age 用于 CORS 相关配置的缓存
                httpURLConnection.setRequestProperty("Access-Control-Max-Age", "3600");
                // 提示OPTIONS预检时,后端需要设置的两个常用自定义头
                httpURLConnection.setRequestProperty("Access-Control-Allow-Headers", "*");
                // 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
                httpURLConnection.setRequestProperty("Access-Control-Allow-Credentials", "true");
                String line;
                InputStream inputStream = httpURLConnection.getInputStream();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                while ((line = bufferedReader.readLine()) != null)
                    content.append(line).append("\n");
                bufferedReader.close();
                inputStream.close();
                // System.out.println(content);
                break;
            } catch (Exception e) {
                System.out.println("第" + count + "获取链接重试!\t" + urls);
                count++;
                e.printStackTrace();
            } finally {
                if (httpURLConnection != null) {
                    httpURLConnection.disconnect();
                }
            }
        }
        if (count > retryCount)
            throw new M3u8Exception("连接超时!");
        return content;
    }

    /**
     * 获取所有的ts片段下载链接
     * @return 链接是否被加密,null为非加密
     */
    private String getTsUrl() throws M3u8Exception, URISyntaxException {
        StringBuilder content = getUrlContent(DOWNLOADURL);
        //判断是否是m3u8链接
        if (!content.toString().contains("#EXTM3U"))
            throw new M3u8Exception(DOWNLOADURL + "不是m3u8链接!");
        String[] split = content.toString().split("\\n");
        String keyUrl = "";
        boolean isKey = false;
        for (String s : split) {
            //如果含有此字段,则说明只有一层m3u8链接
            if (s.contains("#EXT-X-KEY") || s.contains("#EXTINF")) {
                isKey = true;
                keyUrl = DOWNLOADURL;
                break;
            }
            //如果含有此字段,则说明ts片段链接需要从第二个m3u8链接获取
            if (s.contains(".m3u8")) {
                if (s.startsWith("http://") || s.startsWith("https://")) {
                    keyUrl = s;
                } else {
                    String relativeUrl = DOWNLOADURL.substring(0, DOWNLOADURL.lastIndexOf("/") + 1);
                    String relativeUrl2 = DOWNLOADURL.split("//")[0] + "//" +new URI(DOWNLOADURL).getHost();
                    if (s.startsWith("/")) {
                        keyUrl = relativeUrl2 + s;
                    } else {
                        keyUrl = relativeUrl + s;
                    }
                }
                break;
            }
        }
        if (StringUtils.isEmpty(keyUrl))
            throw new M3u8Exception("未发现有效链接!");
        //获取密钥
        String key1 = isKey ? getKey(keyUrl, content) : getKey(keyUrl, null);
        if (StringUtils.isNotEmpty(key1))
            key = key1;
        else key = null;
        return key;
    }

    /**
     * 获取ts解密的密钥,并把ts片段加入set集合
     *
     * @param url     密钥链接,如果无密钥的m3u8,则此字段可为空
     * @param content 内容,如果有密钥,则此字段可以为空
     * @return ts是否需要解密,null为不解密
     */
    private String getKey(String url, StringBuilder content) throws M3u8Exception, URISyntaxException {
        StringBuilder urlContent;
        if (content == null || StringUtils.isEmpty(content.toString()))
            urlContent = getUrlContent(url);
        else urlContent = content;
        if (!urlContent.toString().contains("#EXTM3U"))
            throw new M3u8Exception(DOWNLOADURL + "不是m3u8链接!");
        String[] split = urlContent.toString().split("\\n");
        for (String s : split) {
            //如果含有此字段,则获取加密算法以及获取密钥的链接
            if (s.contains("EXT-X-KEY")) {
                String[] split1 = s.split(",", 2);
                // System.out.println("EXT-X-KEY---length" + split1.length);
                if (split1.length >= 2) {
                    if (split1[0].contains("METHOD"))
                        method = split1[0].split("=", 2)[1];
                    if (split1[1].contains("URI"))
                        key = split1[1].split("=", 2)[1];
                }
            }
        }
        String relativeUrl = url.substring(0, url.lastIndexOf("/") + 1);
        String relativeUrl2 = DOWNLOADURL.split("//")[0] + "//" +new URI(DOWNLOADURL).getHost();
        //将ts片段链接加入set集合
        for (int i = 0; i < split.length; i++) {
            String s = split[i];
            if (s.contains("#EXTINF")) {
                String ts = split[++i];
                if (ts.startsWith("http://") || ts.startsWith("https://")) {
                    tsSet.add(ts);
                } else {
                    if (ts.startsWith("/")) {
                        tsSet.add(relativeUrl2 + ts);
                    } else {
                        tsSet.add(relativeUrl + ts);
                    }
                }
            }
        }
        if (!StringUtils.isEmpty(key)) {
            key = key.replace("\"", "");
            if (key.startsWith("/")) {
                return getUrlContent(relativeUrl2 + key).toString().replaceAll("\\s+", "");
            } else {
                return getUrlContent(relativeUrl + key).toString().replaceAll("\\s+", "");
            }
        }
        return null;
    }

    /**
     * 解密ts
     *
     * @param sSrc ts文件字节数组
     * @param sKey 密钥
     * @return 解密后的字节数组
     */
    private byte[] decrypt(byte[] sSrc, String sKey, String method) throws Exception {
        if (StringUtils.isNotEmpty(method) && !method.contains("AES"))
            throw new M3u8Exception("未知的算法!");
        // 判断Key是否正确
        if (StringUtils.isEmpty(sKey)) {
            return sSrc;
        }
        // 判断Key是否为16位
        if (sKey.length() != 16) {
            System.out.print("Key长度不是16位");
            return null;
        }
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
        SecretKeySpec keySpec = new SecretKeySpec(sKey.getBytes("utf-8"), "AES");
        //如果m3u8有IV标签,那么IvParameterSpec构造函数就把IV标签后的内容转成字节数组传进去
        AlgorithmParameterSpec paramSpec = new IvParameterSpec(new byte[16]);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
        return cipher.doFinal(sSrc);
    }

    /**
     * 开启下载线程
     *
     * @param urls ts片段链接
     * @param i    ts片段序号
     * @return 线程
     */
    private Thread getThread(String urls, int i) {
        return new Thread(() -> {
            int count = 1;
            HttpURLConnection httpURLConnection = null;
            //xy为未解密的ts片段,如果存在,则删除
            File file2 = new File(dir + "/" + i + ".xy");
            if (file2.exists())
                file2.delete();
            OutputStream outputStream = null;
            InputStream inputStream1 = null;
            FileOutputStream outputStream1 = null;
            //重试次数判断
            while (count <= retryCount) {
                try {
                    //模拟http请求获取ts片段文件
                    URL url = new URL(urls);
                    httpURLConnection = (HttpURLConnection) url.openConnection();
                    httpURLConnection.setConnectTimeout((int) timeoutMillisecond);
                    httpURLConnection.setUseCaches(false);
                    httpURLConnection.setReadTimeout((int) timeoutMillisecond);
                    httpURLConnection.setDoInput(true);
                    InputStream inputStream = httpURLConnection.getInputStream();
                    try {
                        outputStream = new FileOutputStream(file2);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                    int len;
                    byte[] bytes = new byte[1024 * 4];
                    //将未解密的ts片段写入文件
                    while ((len = inputStream.read(bytes)) != -1) {
                        outputStream.write(bytes, 0, len);
                        synchronized (this) {
                            downloadBytes = downloadBytes.add(new BigDecimal(len));
                        }
                    }
                    outputStream.flush();
                    inputStream.close();
                    inputStream1 = new FileInputStream(file2);
                    byte[] bytes1 = new byte[inputStream1.available()];
                    inputStream1.read(bytes1);
                    File file = new File(dir + "/" + i + ".xyz");
                    outputStream1 = new FileOutputStream(file);
                    //开始解密ts片段
                    outputStream1.write(decrypt(bytes1, key, method));
                    finishedFiles.add(file);
                    file2.delete();
                    break;
                } catch (Exception e) {
                  System.out.println("第" + count + "获取链接重试!\t" + urls);
                  count++;
                  e.printStackTrace();
                } finally {
                    try {
                        if (inputStream1 != null)
                            inputStream1.close();
                        if (outputStream1 != null)
                            outputStream1.close();
                        if (outputStream != null)
                            outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    if (httpURLConnection != null) {
                        httpURLConnection.disconnect();
                    }
                }
            }
            if (count > retryCount) {
                //自定义异常
                System.out.println("----------连接超时!-------");
                return;
            }
            finishedCount++;
            System.out.println(urls + "下载完毕!\t已完成" + finishedCount + "个,还剩" + (tsSet.size() - finishedCount) + "个!");
        });
    }

    /**
     * 合并下载好的ts片段
     */
    private void mergeTs(BufferedOutputStream bos, BufferedInputStream bis, RandomAccessFile raFile) throws Exception {
        File file = new File(dir + "/" + fileName + ".mp4");
        if (file.exists())
            file.delete();
        else file.createNewFile();
        bos = new BufferedOutputStream(new FileOutputStream(file));
        byte[] b = new byte[4096];
        for (File f : finishedFiles) {
            int len;
            if (!tsSet.iterator().next().endsWith(".ts")) {
                // 破解伪装的非ts文件 向后读取获取真正的ts视频文件
                raFile = new RandomAccessFile(f, "rw");
                raFile.seek(getTsNum(f));
                while ((len = raFile.read(b)) != -1) {
                    bos.write(b, 0, len);
                }
                raFile.close();
            } else {
                bis = new BufferedInputStream(new FileInputStream(f));
                while ((len = bis.read(b)) > 0) {
                    bos.write(b, 0, len);
                }
                bis.close();
            }
            bos.flush();
            f.delete();
        }
        bos.close();
    }

    /**
     * 获取伪png这种ts文件实际字节开始下标
     */
    private int getTsNum(File file) {
        if (num > 0) {
            return num;
        }
        int value = 0;
        List<String> sbHexList = new ArrayList<>();
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            while ((value = is.read()) != -1) {
                // 转换16进制字符
                sbHexList.add(String.format("%02X ", value));
                if (num >= 2) {
                    String flag = sbHexList.get(num-2) + sbHexList.get(num-1) + sbHexList.get(num);
                    flag = flag.toUpperCase();
                    if (flag.equals("FF 47 40 ")) { // ts文件16进制关键标志
                        System.out.println((num + 1) + "----------获取伪png这种ts文件实际字节开始下标--------" + flag);
                        return num + 1;
                    }
                }
                num++;
                // 兜底 防止一直循环
                if (num >= 2000) {
                    break;
                }
            }
            is.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return 0;
    }

    /**
     * 下载视频
     */
    private void startDownload(Handler handler) {
        // 线程池
        final ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadCount);
        int i = 0;
        // 如果生成目录不存在,则创建
        File file1 = new File(dir);
        // System.out.println("生成目录==========" + file1.getAbsolutePath());
        if (!file1.exists())
            file1.mkdirs();
        // 执行多线程下载
        for (String s : tsSet) {
            i++;
            fixedThreadPool.execute(getThread(s, i));
        }
        // 关闭线程池
        fixedThreadPool.shutdown();
        // 下载过程监视
        new Thread(() -> {
            BufferedOutputStream bos = null;
            BufferedInputStream bis = null;
            RandomAccessFile raFile = null;
            try {
                int consume = 0;
                //轮询是否下载成功
                while (!fixedThreadPool.isTerminated()) {
                    try {
                        consume++;
                        BigDecimal bigDecimal = new BigDecimal(downloadBytes.toString());
                        Thread.sleep(1000L);
                        if (tsSet.size() != 0) {
                            String[] arr = new String[]{new BigDecimal(finishedCount).divide(new BigDecimal(tsSet.size()), 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP) + "", id+""};
                            Message msg = handler.obtainMessage(3, arr);
                            handler.sendMessage(msg);
                        }
                        // System.out.print("已用时" + consume + "秒!\t下载速度:" /*+ StringUtils.convertToDownloadSpeed(new BigDecimal(downloadBytes.toString()).subtract(bigDecimal), 3) + "/s"*/);
                        // System.out.print("\t已完成" + finishedCount + "个,还剩" + (tsSet.size() - finishedCount) + "个!");
                        // System.out.println(new BigDecimal(finishedCount).divide(new BigDecimal(tsSet.size()), 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP) + "%");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        String[] arr = new String[]{e.getMessage(), id+""};
                        Message msg= handler.obtainMessage(4, arr);
                        handler.sendMessage(msg);
                    }
                }
                String str = finishedFiles.size() + "个ts文件";
                if (tsSet.size() == finishedFiles.size()) {
                    str = "下载完成,正在合并文件!共" + str;
                } else {
                    str = "部分下载完成,正在合并文件!共" + str + ",实际" + tsSet.size() + "个ts文件";
                }
                String[] arr = new String[]{str, id+""};
                Message msg0 = handler.obtainMessage(3, arr);
                handler.sendMessage(msg0);
                System.out.println(str /*+ StringUtils.convertToDownloadSpeed(downloadBytes, 3)*/);
                // 开始合并视频
                mergeTs(bos, bis, raFile);
                // 下载成功提示
                String[] arr2 = new String[]{"下载文件成功", id+""};
                Message msg = handler.obtainMessage(2, arr2);
                handler.sendMessage(msg);
                System.out.println("视频合并完成,欢迎使用!");
            } catch (Exception e) {
                e.printStackTrace();
                String[] arr = new String[]{e.getMessage(), id+""};
                Message msg= handler.obtainMessage(4, arr);
                handler.sendMessage(msg);
            } finally {
                try {
                    if (bis != null) {
                        bis.close();
                    }
                    if (raFile != null) {
                        raFile.close();
                    }
                    if (bos != null) {
                        bos.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }

    /**
     * 开始下载视频
     */
    public void start(Handler handler) {
        // checkField();
        new Thread(()->{
            try {
                System.out.println(DOWNLOADURL + "   =============");
                String tsUrl = getTsUrl();
                System.out.println("----" + tsUrl);
                if(StringUtils.isEmpty(tsUrl)) {
                    System.out.println("不需要解密");
                }
                startDownload(handler);
            } catch (Exception e) {
                e.printStackTrace();
                String[] arr = new String[]{e.getMessage(), id+""};
                Message msg= handler.obtainMessage(4, arr);
                handler.sendMessage(msg);
            }
        }).start();
    }

    public static void test(String str, Handler handler) {
        new Thread(()-> {
            long thatTime = System.currentTimeMillis();
            while (System.currentTimeMillis() -1000*15 < thatTime) {
                System.out.println("======" + System.currentTimeMillis());
            }
            String[] arr = new String[]{"下载文件成功", 1+""};
            Message msg= handler.obtainMessage(2, arr);
            handler.sendMessage(msg);
            System.out.println("====================   " + str);
        }).start();
    }

    class M3u8Exception extends Exception{
        public M3u8Exception(String message) {
            super(message);
        }
    }

}

六、在Activity中调用

// 获取SD卡的读取权限(Android6.0以上需获取相应权限)
if (ContextCompat.checkSelfPermission(BrowserActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
    // 没有写入外部存储权限 时 申请该权限
    ActivityCompat.requestPermissions(BrowserActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
Intent intent = new Intent(BrowserActivity.this, DownloadService.class);
intent.putExtra("url", url);
if (title.length() > 20) {
    title.substring(0, 20);
}
intent.putExtra("title", title.length() > 20 ? title.substring(0, 20) + "..." : title);
BrowserActivity.this.startService(intent);
feetDialog.dismiss();

参考说明

感谢大佬们的文章!

1、Android 使用Service进行下载并在状态栏显示进度

2、java下载m3u8视频,解密并合并ts(一)

3、记录m3u8变成图片的解决办法

4、如何在Java中将文件转换为十六进制

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值