多线程下载m3u8影视资源 通过ffmpeg合并ts文件为mp4

1 篇文章 0 订阅
1 篇文章 0 订阅

需要用到的ffmpeg,下载地址:Download FFmpeg 

下载以后的目录。

 

 合并、转换、切片都用到了ffmpeg.exe,其它两个我暂时没用到。

<dependency>
         <groupId>com.squareup.okhttp3</groupId>
         <artifactId>okhttp</artifactId>
         <version>3.14.2</version>
 </dependency>
<dependency>
	    <groupId>org.jsoup</groupId>
	    <artifactId>jsoup</artifactId>
	    <version>1.13.1</version>
	</dependency>

java里面用到了:Jsoup、OKHTTP

 以某影视资源提供的m3u8为例:https://vod1.XXX.com/20220424/w9SgsDXT/index.m3u8

package cc.gaole.video.server;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;

import org.apache.commons.collections.CollectionUtils;

import com.gaole.video.DateUtil;
import com.gaole.video.HttpUtils;
import com.gaole.video.resources.ResourcesTemplate;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

/**
*抽象类(因为我实现了几种下载视频的方式,所以定义了一个抽象类,把公共实现的方法都独立出来)
*
**/
public abstract class AbstractAppServer implements Callable<AbstractAppServer>{
	protected String ffmpegPath= null;
	protected String targetFileName = null; //下载的视频定义的名字
	protected String url = null; //下载视频的URL
	protected String rootDir ="F:\\temp"; //下载的ts文件存放目录
	protected String movieDir = "F:\\movie";//ts文件处理完成并且合并成mp4后需要移动的目录
	protected String tempHandleDir = null; //多部电影下载,则每个处理视频,都单独生成一个目录
	protected int corePoolSize = 20;
	protected int queueSize = 3000;
	protected ThreadPoolExecutor excuterManagerPool =null;
	// 单次计数器,用于计算子类里面的线程是否全部处理完了
	protected  CountDownLatch countDownLatch = null;
	/**
    *获取index.m3u8返回的所有ts文件
    */
	protected  List<String> getVideoUrlText(){
    	List<String> list = null;
		try {
			list = ResourcesTemplate.getSuperVideoUrlText(url);
		} catch (Exception e) {
			e.printStackTrace();
			return list;
		}
		
		return list;
    }
	
	protected void downTsFile(List<String> tsList,String rootDir){
    	if(CollectionUtils.isEmpty(tsList)){return;}
    	
    	try {
    		OkHttpClient client= HttpUtils.getHttpClient(true);
        	File dirFile = new File(rootDir);
        	if(!dirFile.exists()){
        		dirFile.mkdirs();
        	}
        	int fileSize = tsList.size();
        	countDownLatch = new CountDownLatch(fileSize);
        	for(int i=0;i<fileSize;i++){
        		String str = tsList.get(i);
        		int tempI = i;
        		excuterManagerPool.submit(new Thread(()->{
        			boolean dowFlag = this.downSignTsFile(client,str,dirFile,1,tempI);
        			countDownLatch.countDown();
            	}));
        	}
			countDownLatch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
    }
    
    private boolean downSignTsFile(OkHttpClient client,String str,File dirFile,int downNum,int downFileName){
    	Request request = new Request.Builder().url(str).get().addHeader("Connection", "keep-alive").build();
    	Response response = null;
		try {
			response = client.newCall(request).execute();
			int responseCode = response.code();
    		if(responseCode == 200){
    			
    			//String fileName = str.substring(str.lastIndexOf("/"),str.lastIndexOf("."));
    			String suffix = str.substring(str.lastIndexOf(".")+1);
    			String newName = dirFile.getAbsolutePath()+"\\"+downFileName+'.'+suffix;
    			byte[] bt = response.body().bytes();
    			File localFile = new File(newName);
    			if(localFile.exists()){
    				localFile.delete();
    			}else{
    				localFile.createNewFile();
    			}
    			
    			Files.write(localFile.toPath(), bt, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
    			return true;
    		}
    		
		} catch (Exception e) {
			if(downNum <=3){
				System.out.println(targetFileName+"第 "+(downFileName+1)+"个文件下载失败:"+e.getMessage()+" ,3秒后重新下载");
				try {
					Thread.currentThread().sleep(3000);
					boolean resultFlag = downSignTsFile(HttpUtils.getHttpClient(true),str,dirFile,downNum+1,downFileName);
					return resultFlag;
				} catch (InterruptedException e1) {
				}
			}else{
				e.printStackTrace();
				
			}
		}finally{
			if(response !=null){
				response.close();
			}
		}
		return false;
    }
    
    /**
     * 删除目录下的文件
     * @param tempHandleDir
     * @param deleteFolder 是否删除目录下的文件夹
     */
    protected boolean deleTempFile(String tempHandleDir,boolean deleteFolder){
    	File tempFile = new File(tempHandleDir);
    	
    	File[] files = tempFile.listFiles();
    	if(files == null){return true;}
    	
    	int fileSize = files.length;
    	if(fileSize <=0){return true;}
    	
    	for(int i=0;i<fileSize;i++){
    		File file = files[i];
    		if(file.isDirectory() && deleteFolder){
    			deleTempFile(file.getPath(),deleteFolder);
    			file.delete();
    		}else if(file.isFile()){
    			file.delete();
    		}
    	}
    	
    	return true;
    }
	
    protected  String createNewFolderName(){
    	return DateUtil.parseStr(Calendar.getInstance().getTime(), DateUtil.FMT_YYYYMMDDHHMM);
    }
    
    protected void fileMoveTarget(File oldFile,String targetMovieDir){
    	File targetDir = new File(targetMovieDir);
    	if(!targetDir.exists()){
    		targetDir.mkdirs();
    	}
    	
    	String moveFileName = oldFile.getName();
    	File targetFile = new File(targetMovieDir+"\\"+moveFileName);
    	if(targetFile.exists()){targetFile.delete();}
    	
    	oldFile.renameTo(targetFile);
   }
}
package cc.gaole.video;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections.CollectionUtils;

import cc.gaole.video.server.AbstractAppServer;

/**
 * Hello world!
 *
 */
public class AppServer extends AbstractAppServer{
	private AppServer(){}
	
	public AppServer(String url,String targetFileName,String ffmpegPath){
		this.url = url.toString();
		this.targetFileName=targetFileName;
		this.ffmpegPath=ffmpegPath;
		excuterManagerPool =new ThreadPoolExecutor(corePoolSize, corePoolSize, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(queueSize));
		tempHandleDir = rootDir+"\\"+this.createNewFolderName()+(new Random().nextInt()*1000);
	}
	
	@Override
	public AppServer call() throws Exception {
		System.out.println(targetFileName+"开始时间:"+DateUtil.parseStr(new Date(), DateUtil.FMT_YYYYMMDD_HHMMSS));
		this.deleTempFile(tempHandleDir, true);
		if(m3u8Flag){
			downM3U8(tempHandleDir);
		}else{
			downVideo(tempHandleDir);
		}
		this.deleTempFile(tempHandleDir, true);
    	//把生成的根目录删掉
    	new File(tempHandleDir).delete();
		System.out.println(targetFileName+"结束时间:"+DateUtil.parseStr(new Date(), DateUtil.FMT_YYYYMMDD_HHMMSS));
		return this;
	}
	
	private void downM3U8(String tempHandleDir){
    	List<String> tsList = this.getVideoUrlText();
    	if(CollectionUtils.isEmpty(tsList) || tsList.size() <=0){System.out.println(targetFileName+"未获取到片源");return;}
    	
    	this.downTsFile(tsList,tempHandleDir);
    	//合并、移动文件
    	this.mergeTs(tempHandleDir);
	}
    
    //合并文件
    private void mergeTs(String tsDir){
    	File tsDirFile = new File(tsDir);
    	File[] files = tsDirFile.listFiles();
    	int fileSize = files.length;
    	if(fileSize <=0){return;}
    	
    	Arrays.sort(files, new Comparator<File>() {

			@Override
			public int compare(File f1, File f2) {
				int f1Num =Integer.parseInt((f1.getName().substring(0, f1.getName().indexOf("."))));
				int f2Num =Integer.parseInt((f2.getName().substring(0, f2.getName().indexOf("."))));
				long diff = f1Num - f2Num; //f1.lastModified() - f2.lastModified();
                if (diff > 0)
                    return 1;
                else if (diff == 0)
                    return 0;
                else
                    return -1;//如果 if 中修改为 返回-1 同时此处修改为返回 1  排序就会是递减,如果 if 中修改为 返回1 同时此处修改为返回 -1  排序就会是递增,
			}
    		
    	});
    	
    	//创建一个完成目录
    	File succesDir = new File(tsDir+"\\完成");
    	if(succesDir.exists()){
    		succesDir.delete();
    	}
    	succesDir.mkdir();
    	
    	File endFile = new File(succesDir+"\\"+targetFileName+".ts");
    	if (endFile.exists())
    		endFile.delete();
        else {
        	try {
        		endFile.createNewFile();
			} catch (Exception e) {
			}
        }
    	
    	FileOutputStream fileOutputStream = null;
    	try {
    		fileOutputStream = new FileOutputStream(endFile);
    		byte[] b = new byte[4096];
        	
        	for(int i=0;i<fileSize;i++){
        		File file = files[i];
        		FileInputStream fileInputStream = new FileInputStream(file);
        		 
        		int len;
                while ((len = fileInputStream.read(b)) != -1) {
                    fileOutputStream.write(b, 0, len);
                }
                fileInputStream.close();
                fileOutputStream.flush();
                
        	}
        	fileOutputStream.close();
		} catch (Exception e) {
			e.printStackTrace();
			if(fileOutputStream !=null){
				try {
					fileOutputStream.close();
				} catch (IOException e1) {
				}
			}
		}
    	
    	//视频转码成mp4
    	File convertVideoFile = this.fileConvertVideo(endFile);
    	//文件移动
    	if(convertVideoFile !=null && convertVideoFile.exists()){
    		this.fileMoveTarget(convertVideoFile, movieDir);
    	}else{
    		System.out.println(targetFileName+"格式转换失败");
    	}
    }
    
    private File fileConvertVideo(File oldFile){
    	 Runtime runtime = null;
    	 File mp4File = new File(oldFile.getParent()+"\\"+(oldFile.getName().substring(0,oldFile.getName().indexOf(".")))+".mp4");
    	try {
    		StringBuilder sb = new StringBuilder(this.ffmpegPath);
        	sb.append(" -y");
        	sb.append(" -i");
        	sb.append(" "+oldFile.getAbsolutePath());
        	sb.append(" -threads 2");
        	sb.append("  -c:v copy -c:a copy");
        	sb.append(" "+mp4File.getAbsolutePath());
        	runtime = Runtime.getRuntime();
			Process process =runtime.exec(sb.toString());
			BufferedReader br = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line = null;
            while((line = br.readLine()) != null){
            	//System.out.println("<<视频执行信息>> "+line);
            }
            br.close();
            process.getOutputStream().close();
            process.getInputStream().close();
            process.getErrorStream().close();
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		} finally{
		}
    	return mp4File;
    }
    
}

main方法调用

package cc.gaole.video;

import java.io.File;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import cc.gaole.video.server.AbstractAppServer;

 * 2022年6月20日
 */
public class VideoCoreApp {
	private static final String ffmpegPath = "F:\\ffmpeg-4.4.1-essentials_build\\ffmpeg-4.4.1-essentials_build\\bin\\ffmpeg.exe"; 
	private ThreadPoolExecutor excuterManagerPool =null;
	public VideoCoreApp(){
		excuterManagerPool =new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(100));
	}
	
	public void addTask(AbstractAppServer appServer){
		this.excuterManagerPool.submit(appServer);
	}
	
	public static void main(String[] args) {
		
		try {
			
   			String ffmpegPath = "F:\\ffmpeg-4.4.1-essentials_build\\ffmpeg-4.4.1-essentials_build\\bin\\ffmpeg.exe"; 
   			VideoCoreApp core = new VideoCoreApp();
   			core.addTask(new AppServer("https://vod1.xxx.com/20220424/w9SgsDXT/index.m3u8", "仙武帝尊第一集",ffmpegPath));
   			
		
   		} catch (Exception e) {
   			e.printStackTrace();
   		}
		
	}
}

里面掺杂了很多自己的东西,所以不想单独分解出来了,主要为了自己做个笔记记录起来,方便以后可以参阅。

main方法里面定义了一个线程池,可以一次性下载几部资源,在子类里面AppServer也定义了一个线程池可以一次性处理多少个ts文件。

执行流程:

1、VideoCoreApp 的 main方法 初始化主线程

2、AppServer 的构造方法 初始化子线程

3、 AppServer 的call()   --- 主方法

4、AppServer  的downM3U8()  --- 主方法的具体实现方法

5、AbstractAppServer 的 getVideoUrlText() --- 获取index.m3u8需要下载的ts文件  和 downTsFile() --- 下载ts文件

6、AppServer 的 mergeTs()   --- 将下载好的ts文件合并成mp4文件,并且移动到一个完成的目录

需要用到的工具类

package cc.gaole.video.resources;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
//获取资源网站ts文件
public abstract class ResourcesTemplate {

	public String parseUrl = null;
	public String url = "";
	public abstract  List<String> getVideoUrlText(String url) throws Exception;
	
	public static List<String> getSuperVideoUrlText(String url) throws Exception{
        //有些资源网站比较特殊,要单独处理,这里只给大家提供通用的即可
		ResourcesTemplate resources =  new CommonResources(url);

		return resources.getVideoUrlText(url);
	}
	
	public  List<String> getTsUrlList(String resultText,String absolutePrefixPath) {
		List<String> list = new ArrayList<String>();
		String [] strs = resultText.split(",");
		for(String urlStr :strs){
			if(!urlStr.contains(".ts")){continue;}
			Pattern pattern = Pattern.compile("([http|https]?.*\\.ts)");
			Matcher m = pattern.matcher(urlStr);
			if (m.find()) {
				String urlTemp = m.group(0);
				if(urlTemp.charAt(0) != '/'){
					urlTemp = "/"+urlTemp;
				}
				list.add(urlTemp.startsWith("http") ? urlTemp : ((StringUtils.isEmpty(absolutePrefixPath) ? parseUrl : absolutePrefixPath)+urlTemp));
			}
		}
		return list;
	}
}
package cc.gaole.video.resources;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jsoup.Jsoup;

import cc.gaole.video.FileUtil;
import cc.gaole.video.HttpUtils;
import cc.gaole.video.SSLSocketClient;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class CommonResources extends ResourcesTemplate{
	
	public CommonResources(String url){
		this.url = url;
	}

	@Override
	public List<String> getVideoUrlText(String url) throws Exception {
		List<String> list = null;
		OkHttpClient client= HttpUtils.getHttpClient(true);
		Response response = null;
		try {
			Request request = new Request.Builder().url(url).get().addHeader("Connection", "keep-alive").build();
			response = client.newCall(request).execute();
			int responseCode = response.code();
			if(responseCode == 200){
				String urlText  = response.body().string();
				if(urlText.contains("EXTM3U") && urlText.length() >20){
					URL host = new URL(url);
					if(urlText.contains(".m3u8")){
						//有的资源要进行第二次获取ts的文件
						Pattern pattern = Pattern.compile("([a-z|A-Z|/|0-9]+.*.m3u8)");
						Matcher m = pattern.matcher(urlText);
						if (m.find()) {
							urlText  = m.group(0); 
							if(urlText.charAt(0) != '/'){
								urlText = "/"+urlText;
							}						
							if(!urlText.contains("http")){
								urlText = HttpUtils.getHostAbsolutePath(host)+ urlText;
							}
							
							SSLSocketClient.uncheckSSL();
							urlText = Jsoup.connect(urlText).ignoreContentType(true).execute().body();  
						}
					}
					
					list = super.getTsUrlList(urlText, HttpUtils.getHostAbsolutePath(host));
				}
			}else{
				System.out.println(url+"资源访问返回状态:"+responseCode);
			}
		} catch (Exception e) {
			throw new Exception(e);
		} finally{
			if(response !=null){
				response.close();
			}
		}
		
		return list;
	}

}
package cc.gaole.video;

import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

import okhttp3.ConnectionPool;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;

public class HttpUtils {
	
	private static final int connectTimeout= 10000;
	private static final int readTimeout = 10000;
	private static final int writeTimeout=10000;
	private static ConnectionPool mConnectionPool=new ConnectionPool(1000, 30, TimeUnit.MINUTES);
	
	public static OkHttpClient getHttpClient(boolean ignoreSSLChecked){
		Builder builder = new OkHttpClient.Builder().cookieJar(new CookieJar() {
			private final HashMap<String, List<Cookie>> cookieStore = new HashMap<String, List<Cookie>>();
			@Override
			public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
				cookieStore.put(url.host(), cookies);
				
			}
			
			@Override
			public List<Cookie> loadForRequest(HttpUrl url) {
				List<Cookie> cookies = cookieStore.get(url.host());
		        return cookies != null ? cookies : new ArrayList<Cookie>();
			}
		})
		.followRedirects(false)
		.followSslRedirects(false)
		.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS) //连接超时
        .readTimeout(readTimeout, TimeUnit.MILLISECONDS) //读取超时
        .writeTimeout(writeTimeout, TimeUnit.MILLISECONDS) //写超时
        .connectionPool(mConnectionPool);
		if(ignoreSSLChecked){
			  //忽略SSL验证
			builder.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(),SSLSocketClient.getX509TrustManager())
	    		   .hostnameVerifier(SSLSocketClient.getHostnameVerifier());
		}
		
		OkHttpClient httpClient =builder.build();
		return httpClient;
	}
	
	public static String getHostAbsolutePath(URL host){
		Integer port  = host.getPort();
		String urlPath = host.getProtocol()+"://"+ host.getHost()+(port !=null && port.intValue() >=1 ? ":"+port : ""); 
		return urlPath;
	}
}
package cc.gaole.video;

import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
/**
 *  不想再本地使用证书访问网站,可以忽略SSL验证
 */
public class SSLSocketClient {
	
	/**
	 * 忽略SSL验证,需配合jsonup 或者 httpConnection
	 */
	static public void uncheckSSL() {
        try {
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, new X509TrustManager[]{new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
        } catch (NoSuchAlgorithmException e) {
        } catch (KeyManagementException e) {
        }
    }

	//获取这个SSLSocketFactory
    public static SSLSocketFactory getSSLSocketFactory() {
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, getTrustManager(), new SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
  //获取TrustManager
    private static TrustManager[] getTrustManager() {
        return new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }
 
                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    }
 
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }
        };
    }
 
    //获取HostnameVerifier
    public static HostnameVerifier getHostnameVerifier() {
        return (s, sslSession) -> true;
    }
    
    public static X509TrustManager getX509TrustManager() {
        X509TrustManager trustManager = null;
        try {
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init((KeyStore) null);
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
            }
            trustManager = (X509TrustManager) trustManagers[0];
        } catch (Exception e) {
            e.printStackTrace();
        }
 
        return trustManager;
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值