下載m3u8視頻并將ts合并為mp4格式

定義

定義

視頻除了常見(jiàn)格式mp4flv之外,還有m3u8格式。m3u8是蘋(píng)果公司推出一種視頻播放標(biāo)準(zhǔn),是m3u的一種,不過(guò)編碼方式是utf-8,是一種文件檢索格式,將視頻切割成一小段一小段的ts格式的視頻文件,然后存在服務(wù)器中(現(xiàn)在為了減少I(mǎi)/o訪問(wèn)次數(shù),一般存在服務(wù)器的內(nèi)存中),通過(guò)m3u8解析出來(lái)路徑,然后去請(qǐng)求。這樣每次請(qǐng)求很小一段視頻,可以做到近似于實(shí)時(shí)播放的效果。

分析

1、視頻播放地址必須是m3u8鏈接。當(dāng)播放視頻的時(shí)候,如果你打開(kāi)了瀏覽器的開(kāi)發(fā)者工具的話,就會(huì)發(fā)現(xiàn)有許多的ts片段。這些ts片段也就是加載的視頻片段。我們要做的就是下載這些ts片段,然后合并。
2、當(dāng)你打開(kāi)m3u8鏈接的時(shí)候,會(huì)發(fā)現(xiàn)m3u8實(shí)際上是一個(gè)可以用文本打開(kāi)的一個(gè)文件,它包含了一些和視頻相關(guān)的標(biāo)簽。通過(guò)這些標(biāo)簽,我們可以獲取我們要下載的ts片段。
3、現(xiàn)在大部分網(wǎng)站都對(duì)ts片段進(jìn)行加密,所以我們首先要從m3u8文件拿到ts密鑰。然后再進(jìn)行下載,當(dāng)然有的ts片段是沒(méi)有被加密的。
4、每一個(gè)解密后ts片段都是可以單獨(dú)播放的,所以合并的時(shí)候我們就直接流合并就行了,無(wú)需做任何處理,合并的文件我們就用mp4。

優(yōu)點(diǎn)

可以識(shí)別m3u8獲取的ts片段是否需要解密
可以自定義下載線程數(shù),達(dá)到多線程快速下載
可以自定義ts片段下載失敗重試次數(shù),很難下載失敗

缺點(diǎn)

當(dāng)重試次數(shù)耗盡時(shí)或者部分片段解密失敗時(shí),不能夠再次重新下載失敗的ts片段。但是不影響視頻后期合并,導(dǎo)致觀看合并完成的視頻的時(shí)候,播放不銜接;
線程越多,占用內(nèi)存越高。當(dāng)線程數(shù)為100時(shí),下載400M視頻需要700M內(nèi)存,而10個(gè)線程則需要70M左右內(nèi)存。當(dāng)然線程越多,下載越快,需要自行權(quán)衡。

示例

示例:

http://cdn.can.cibntv.net/12/201702161000/rexuechangan01/1.m3u8

請(qǐng)求:

模擬HTTP請(qǐng)求,獲取鏈接相應(yīng)內(nèi)容

 /**
  * 模擬http請(qǐng)求獲取內(nèi)容
  *  
  * @param urls http鏈接
  * @return 內(nèi)容
  */
  private StringBuilder getUrlContent(String urls) {
    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);
        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("連接超時(shí)!");
        return content;
      }
    }
響應(yīng):
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:14
#EXTINF:11.480, 
20170215T224129-1-0.ts
#EXTINF:11.480, 
20170215T224129-1-1.ts
#EXTINF:10.480, 
20170215T224129-1-2.ts
#EXTINF:11.400, 
20170215T224129-1-3.ts
#EXTINF:11.120, 
20170215T224129-1-4.ts
#EXTINF:11.200, 
20170215T224129-1-5.ts
#EXTINF:13.600, 
20170215T224129-1-6.ts
#EXTINF:11.360, 
20170215T224129-1-7.ts
#EXTINF:10.240, 
20170215T224129-1-8.ts
#EXTINF:12.000, 
20170215T224129-1-9.ts
#EXTINF:13.760, 
20170215T224129-1-10.ts
#EXT-X-ENDLIST

#EXT-X-ENDLIST標(biāo)識(shí)ts結(jié)尾的文件,這才是視頻真正的存放路徑:http://cdn.can.cibntv.net/12/201702161000/rexuechangan01/20170215T224129-1-0.ts ,這時(shí)候用瀏覽器下載就可以播放。不過(guò)這個(gè)播放不用我們?nèi)ソ馕?android 4.0以后的videoView 就支持自動(dòng)解析,并拼接播放。

TAG含義

M3U8格式講解及實(shí)際應(yīng)用分析
流媒體開(kāi)發(fā)之--HLS--M3U8解析(2): HLS草案

判斷是否需要解密

首先將m3u8鏈接內(nèi)容通過(guò)getUrlContent()方法獲取到,然后解析,如果內(nèi)容含有#EXT-X-KEY標(biāo)簽,則說(shuō)明這個(gè)鏈接是需要進(jìn)行ts文件解密的,然后通過(guò)下面的m3u8if語(yǔ)句獲取含有密鑰以及ts片段的鏈接。
如果含有#EXTINF則說(shuō)明這個(gè)鏈接就是含有ts視頻片段的鏈接,沒(méi)有第二個(gè)m3u8鏈接了。
之后我們要獲取密鑰的getKey方法,即時(shí)不需要密鑰。并把ts片段加進(jìn)set集合,即tsSet字段。

/**
 * 獲取所有的ts片段下載鏈接
 *
 * @return 鏈接是否被加密,null為非加密
 */
 private String getTsUrl() {
   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) {
     // 如果含有此字段,則說(shuō)明只有一層m3u8鏈接
     if (s.contains("#EXT-X-KEY") || s.contains("#EXTINF")) {
       isKey = true;
       keyUrl = DOWNLOADURL;
       break;
     }
     // 如果含有此字段,則說(shuō)明ts片段鏈接需要從第二個(gè)m3u8鏈接獲取
     if (s.contains(".m3u8")) {
       if (StringUtils.isUrl(s)) {
         return s;
       }
       String relativeUrl = DOWNLOADURL.substring(0, DOWNLOADURL.lastIndexOf("/") + 1);
       keyUrl = relativeUrl + s;
       break;
     }
   }
   if (StringUtils.isEmpty(keyUrl)) {
     throw new M3u8Exception("未發(fā)現(xiàn)key鏈接!");
   }
   // 獲取密鑰
   String key1 = isKey ? getKey(keyUrl, content) : getKey(keyUrl, null);
   if (StringUtils.isNotEmpty(key1)) {
     key = key1;
   } else {
     key = null;
   }
   return key;
 }
獲取密鑰

如果參數(shù)content不為空,則說(shuō)明密鑰信息從此字段取,否則則訪問(wèn)第二個(gè)m3u8鏈接,然后獲取信息。
也就是說(shuō),如果content為空,說(shuō)明則為樣例一,三的情況,第一個(gè)m3u8文件里面沒(méi)有ts片段信息,需要從第二個(gè)m3u8文件取。
如果發(fā)現(xiàn)不需要解密,此方法將會(huì)返回null。需要解密的話,那么解密算法將會(huì)存在method字段,密鑰將存在key字段。

/**
 * 獲取ts解密的密鑰,并把ts片段加入set集合
 *
 * @param url     密鑰鏈接,如果無(wú)密鑰的m3u8,則此字段可為空
 * @param content 內(nèi)容,如果有密鑰,則此字段可以為空
 * @return ts是否需要解密,null為不解密
 */
 private String getKey(String url, StringBuilder content) {
   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);
       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);
   // 將ts片段鏈接加入set集合
   for (int i = 0; i < split.length; i++) {
     String s = split[i];
     if (s.contains("#EXTINF")) {
       tsSet.add(relativeUrl + split[++i]);
     }
   }
   if (!StringUtils.isEmpty(key)) {
     key = key.replace("\"", "");
     return getUrlContent(relativeUrl + key).toString().replaceAll("\\s+", "");
   }
   return null;
 }
解密ts:
/**
 * 解密ts
 *
 * @param sSrc ts文件字節(jié)數(shù)組
 * @param sKey 密鑰
 * @return 解密后的字節(jié)數(shù)組
 */
 private static byte[] decrypt(byte[] sSrc, String sKey, String method) {
   try {
     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長(zhǎng)度不是16位");
       return null;
     }
     Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
     SecretKeySpec keySpec = new SecretKeySpec(sKey.getBytes("utf-8"), "AES");
     // 如果m3u8有IV標(biāo)簽,那么IvParameterSpec構(gòu)造函數(shù)就把IV標(biāo)簽后的內(nèi)容轉(zhuǎn)成字節(jié)數(shù)組傳進(jìn)去
     AlgorithmParameterSpec paramSpec = new IvParameterSpec(new byte[16]);
     cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
     return cipher.doFinal(sSrc);
   } catch (Exception ex) {
     ex.printStackTrace();
     return null;
   }
 }
啟動(dòng)線程下載ts片段

代碼中xy后綴文件是未解密的ts片段,xyz是解密后的ts片段,這兩個(gè)后綴起成什么無(wú)所謂。

/**
 * 開(kāi)啟下載線程
 *
 * @param urls ts片段鏈接
 * @param i    ts片段序號(hào)
 * @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;
     // 重試次數(shù)判斷
     while (count <= retryCount) {
       try {
         // 模擬http請(qǐng)求獲取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];
         // 將未解密的ts片段寫(xiě)入文件
         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);
         //開(kāi)始解密ts片段,這里我們把ts后綴改為了xyz,改不改都一樣
         outputStream1.write(decrypt(bytes1, key, method));
         finishedFiles.add(file);
         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) {
       // 自定義異常
       throw new M3u8Exception("連接超時(shí)!");
     }
     finishedCount++;
     System.out.println(urls + "下載完畢!\t已完成" + finishedCount + "個(gè),還剩" + (tsSet.size() - finishedCount) + "個(gè)!");
   });
 }
合并以及刪除多余的ts片段
/**
 * 合并下載好的ts片段
 */
 private void mergeTs() {
   try {
     File file = new File(dir + "/" + fileName + ".mp4");
     if (file.exists()) {
       file.delete();
     } else {
       file.createNewFile();
     }
     FileOutputStream fileOutputStream = new FileOutputStream(file);
     byte[] b = new byte[4096];
     for (File f : finishedFiles) {
       FileInputStream fileInputStream = new FileInputStream(f);
       int len;
       while ((len = fileInputStream.read(b)) != -1) {
         fileOutputStream.write(b, 0, len);
       }
       fileInputStream.close();
       fileOutputStream.flush();
     }
     fileOutputStream.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

 /**
  * 刪除下載好的片段
  */
  private void deleteFiles() {
    File file = new File(dir);
    for (File f : file.listFiles()) {
      if (!f.getName().contains(fileName + ".mp4")) {
        f.deleteOnExit();
      }
    }
  }
播放:
Uri uri =     Uri.parse("http://cdn.can.cibntv.net/12/201702161000/rexuechangan01/rexuechangan01.m3u8");
video_view.setMediaController(new MediaController(this));
video_view.setVideoURI(uri);  
video_view.requestFocus();
ideo_view.start();

參考:

源碼:M3U8Download
源碼:M3U8Downloader
java下載m3u8視頻,解密并合并ts(一)
java下載m3u8視頻,解密并合并ts(二)
java下載m3u8視頻,解密并合并ts(三)

例子:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容