使用JavaCV實現海康rtsp轉rtmp實現無插件web端直播(無需轉碼,低資源消耗)

項目碼雲(Gitee)地址:https://gitee.com/banmajio/RTSPtoRTMP
項目github地址:https://github.com/banmajio/RTSPtoRTMP
個人博客:banmajio’s blog

瀏覽器不支持flash插件之後,h5播放rtmp直播流的解決方案

參考:javaCV開發詳解之8:轉封裝在rtsp轉rtmp流中的應用(無須轉碼,更低的資源消耗)

【注意】該項目只能用來實現直播的rtsp轉rtmp,無法滿足回放需求。對於海康設備來說,rtsp指令帶參數進行回放,會發生報:帶寬不足的問題。所以需要回放功能的請對海康sdk進行二次開發,手動捕獲碼流數據轉封裝爲rtmp流。實現思路請參考:海康sdk捕獲碼流數據通過JavaCV推成rtmp流的實現思路(PS流轉封裝RTMP) 海康sdk二次開發推rtmp流的項目已經開發優化完成,但暫不考慮開源,有需要的請聯繫q:1402325991 有償!! 介意勿擾!!

用到的技術:FFmpeg、JavaCV、ngingx
項目背景:將海康攝像頭的rtsp流轉爲rtmp流,配合video.js實現web端播放。
[注]:該項目中的一些處理是爲了滿足公司項目需求添加完善的,如果需要改造擴展只需要在原來的基礎上進行擴充或者剝離即可。最基本的核心操作在CameraPush.java這個類中,或者參考上述鏈接原作者的代碼。

該項目需要搭配使用的nginx服務器下載地址:http://cdn.banmajio.com/nginx.rar
下載後解壓該文件,點擊nginx.exe(閃退是正常的,可以通過任務管理器查看是否存在nginx進程,存在則說明啓動成功了)啓動nginx服務。
nginx的配置文件存放在conf目錄下的nginx.conf,根據需要修改。項目中的rtmp地址就是根據這個配置文件來的。

上述bug優化1:JavaCV中FFmpegFrameGrabber調用start()方法時出現阻塞的解決辦法

項目github地址:https://github.com/banmajio/RTSPtoRTMP
個人博客:banmajio’s blog

目錄結構

目錄結構

1.com.junction包裏的類爲SpringBoot項目啓動類。
2.com.junction.cache包裏的類爲保存推流信息的緩存類。
3.com.junction.controller包裏的類爲項目controller API接口。
4.com.junction.pojo包裏的類爲相機信息和配置文件映射的bean。
5.com,junction.thread包裏的類爲線程池管理類。
6.com.junction.util包裏的類爲拉流推流業務處理類和定時任務Timer類。
7.application.yml爲項目配置文件。

添加依賴,編寫配置文件

1.添加依賴,引入javacpp和ffmpeg的jar包。

		<!-- javacv1.5.1 -->
		<dependency>
			<groupId>org.bytedeco</groupId>
			<artifactId>javacv</artifactId>
			<version>1.5.1</version>
		</dependency>
		<dependency>
			<groupId>org.bytedeco</groupId>
			<artifactId>ffmpeg-platform</artifactId>
			<version>4.1.3-1.5.1</version>
		</dependency>
		<!-- 支持 @ConfigurationProperties 註解 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>

2.pom中引入的spring-boot-configuration-processor是爲了將配置文件映射爲bean,方便項目中使用配置文件中的值

server:
  port: 8082
  servlet:
   context-path: /camera  
  
config:
#直播流保活時間(分鐘)
  keepalive: 5
#nginx推送地址
  push_ip: 127.0.0.1
#nginx推送端口
  push_port: 1935

創建Bean

1.CameraPojo(相機信息)

	private String username;// 攝像頭賬號
	private String password;// 攝像頭密碼
	private String ip;// 攝像頭ip
	private String channel;// 攝像頭通道號
	private String stream;// 攝像頭碼流(main爲主碼流、sub爲子碼流)
	private String rtsp;// rtsp地址
	private String rtmp;// rtmp地址
	private String startTime;// 回放開始時間
	private String endTime;// 回放結束時間
	private String openTime;// 打開時間
	private int count = 0;// 使用人數
	private String token;//唯一標識token

2.Config(讀取配置文件的bean)

package com.junction.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @Title ConfigPojo.java
 * @description 讀取配置文件的bean
 * @time 2019年12月25日 下午5:11:21
 * @author wuguodong
 **/
@Component
//讀取application.yml中config層級下的配置項
@ConfigurationProperties(prefix = "config")
public class Config {
	private String keepalive;//保活時長(分鐘)
	private String push_ip;//推送地址
	private String push_port;//推送端口
	
	public String getKeepalive() {
		return keepalive;
	}
	public void setKeepalive(String keepalive) {
		this.keepalive = keepalive;
	}
	public String getPush_ip() {
		return push_ip;
	}
	public void setPush_ip(String push_ip) {
		this.push_ip = push_ip;
	}
	public String getPush_port() {
		return push_port;
	}
	public void setPush_port(String push_port) {
		this.push_port = push_port;
	}
	@Override
	public String toString() {
		return "Config [keepalive=" + keepalive + ", push_ip=" + push_ip + ", push_port=" + push_port + "]";
	}	
}

創建緩存Cache

保存推流信息,與服務啓動的時間。

/**
 * @Title CacheUtil.java
 * @description 推流緩存信息
 * @time 2019年12月17日 下午3:12:45
 * @author wuguodong
 **/
public final class CacheUtil {
	/*
	 * 保存已經開始推的流
	 */
	public static Map<String, CameraPojo> STREAMMAP = new ConcurrentHashMap<String, CameraPojo>();

	/*
	 * 保存服務啓動時間
	 */
	public static long STARTTIME;
}

修改啓動類

項目啓動時,將啓動時間存入緩存中;項目結束時,銷燬線程池和定時器,釋放資源。

package com.junction;

import java.util.Date;

import javax.annotation.PreDestroy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.junction.cache.CacheUtil;
import com.junction.thread.CameraThread;
import com.junction.util.TimerUtil;

@SpringBootApplication
public class CameraServerApplication {

	public static void main(String[] args) {
		//將服務啓動時間存入緩存
		CacheUtil.STARTTIME = new Date().getTime();
		SpringApplication.run(CameraServerApplication.class, args);
	}

	@PreDestroy
	public void destory() {
		System.err.println("釋放空間...");
		// 關閉線程池
		CameraThread.MyRunnable.es.shutdownNow();
		// 銷燬定時器
		TimerUtil.timer.cancel();
	}
}

拉流、推流、轉封裝

1.兩個重要構造器FFmpegFrameGrabberFFmpegFrameRecorder
2.轉封裝不涉及轉碼,所以資源佔用很低。

什麼是轉封裝?爲什麼轉封裝比轉碼消耗更少?爲什麼轉封裝無法改動視頻尺寸?
先舉個栗子:假設視頻格式(mp4,flv,avi等)是盒子,裏面的視頻編碼數據(h264,hevc)是蘋果,我們把這個蘋果從盒子裏取出來放到另一個盒子裏,盒子是變了,蘋果是沒有變動的,因此視頻相關的尺寸數據是沒有改動的,這個就是轉封裝的概念。
有了上面這個例子,我們可以把“轉碼”理解爲:把這個盒子裏的蘋果(hevc)拿出來削皮切塊後再加工成櫻桃(h264)後再裝到另一個盒子裏,多了一步對蘋果(hevc)轉換爲櫻桃(h264)的操作,自然比直接把蘋果拿到另一個盒子(轉封裝)要消耗更多機器性能。


import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;

import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;

import com.junction.pojo.CameraPojo;

/**
 * @Title CameraPush.java
 * @description 拉流推流
 * @time 2019年12月16日 上午9:34:41
 * @author wuguodong
 **/
public class CameraPush {
	protected FFmpegFrameGrabber grabber = null;// 解碼器
	protected FFmpegFrameRecorder record = null;// 編碼器
	int width;// 視頻像素寬
	int height;// 視頻像素高

	// 視頻參數
	protected int audiocodecid;
	protected int codecid;
	protected double framerate;// 幀率
	protected int bitrate;// 比特率

	// 音頻參數
	// 想要錄製音頻,這三個參數必須有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0
	private int audioChannels;
	private int audioBitrate;
	private int sampleRate;

	// 設備信息
	private CameraPojo cameraPojo;

	public CameraPush(CameraPojo cameraPojo) {
		this.cameraPojo = cameraPojo;
	}
	/**
	 * 選擇視頻源
	 * 
	 * @author wuguodong
	 * @throws Exception
	 */
	public CameraPush from() throws Exception {
		// 採集/抓取器
		System.out.println(cameraPojo.getRtsp());
		grabber = new FFmpegFrameGrabber(cameraPojo.getRtsp());
		if (cameraPojo.getRtsp().indexOf("rtsp") >= 0) {
			grabber.setOption("rtsp_transport", "tcp");// tcp用於解決丟包問題
		}
		// 設置採集器構造超時時間
		grabber.setOption("stimeout", "2000000");
		grabber.start();// 開始之後ffmpeg會採集視頻信息,之後就可以獲取音視頻信息
		width = grabber.getImageWidth();
		height = grabber.getImageHeight();
		// 若視頻像素值爲0,說明採集器構造超時,程序結束
		if (width == 0 && height == 0) {
			System.err.println("[ERROR]   拉流超時...");
			return null;
		}
		// 視頻參數
		audiocodecid = grabber.getAudioCodec();
		System.err.println("音頻編碼:" + audiocodecid);
		codecid = grabber.getVideoCodec();
		framerate = grabber.getVideoFrameRate();// 幀率
		bitrate = grabber.getVideoBitrate();// 比特率
		// 音頻參數
		// 想要錄製音頻,這三個參數必須有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0
		audioChannels = grabber.getAudioChannels();
		audioBitrate = grabber.getAudioBitrate();
		if (audioBitrate < 1) {
			audioBitrate = 128 * 1000;// 默認音頻比特率
		}
		return this;
	}
	/**
	 * 選擇輸出
	 * 
	 * @author wuguodong
	 * @throws Exception
	 */
	public CameraPush to() throws Exception {
		// 錄製/推流器
		record = new FFmpegFrameRecorder(cameraPojo.getRtmp(), width, height);
		record.setVideoOption("crf", "28");// 畫面質量參數,0~51;18~28是一個合理範圍
		record.setGopSize(2);
		record.setFrameRate(framerate);
		record.setVideoBitrate(bitrate);

		record.setAudioChannels(audioChannels);
		record.setAudioBitrate(audioBitrate);
		record.setSampleRate(sampleRate);
		AVFormatContext fc = null;
		if (cameraPojo.getRtmp().indexOf("rtmp") >= 0 || cameraPojo.getRtmp().indexOf("flv") > 0) {
			// 封裝格式flv
			record.setFormat("flv");
			record.setAudioCodecName("aac");
			record.setVideoCodec(codecid);
			fc = grabber.getFormatContext();
		}
		record.start(fc);
		return this;
	}

	/**
	 * 轉封裝
	 * 
	 * @author wuguodong
	 * @throws org.bytedeco.javacv.FrameGrabber.Exception
	 * @throws org.bytedeco.javacv.FrameRecorder.Exception
	 * @throws InterruptedException
	 */
	public CameraPush go(Thread nowThread)
			throws org.bytedeco.javacv.FrameGrabber.Exception, org.bytedeco.javacv.FrameRecorder.Exception {
		long err_index = 0;// 採集或推流導致的錯誤次數
		// 連續五次沒有采集到幀則認爲視頻採集結束,程序錯誤次數超過5次即中斷程序
		//將探測時留下的數據幀釋放掉,以免因爲dts,pts的問題對推流造成影響
		grabber.flush();
		for (int no_frame_index = 0; no_frame_index < 5 || err_index < 5;) {
			try {
				// 用於中斷線程時,結束該循環
				nowThread.sleep(1);
				AVPacket pkt = null;
				// 獲取沒有解碼的音視頻幀
				pkt = grabber.grabPacket();
				if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
					// 空包記錄次數跳過
					no_frame_index++;
					err_index++;
					continue;
				}
				// 不需要編碼直接把音視頻幀推出去
				err_index += (record.recordPacket(pkt) ? 0 : 1);
				av_packet_unref(pkt);
			} catch (InterruptedException e) {
				// 當需要結束推流時,調用線程中斷方法,中斷推流的線程。當前線程for循環執行到
				// nowThread.sleep(1);這行代碼時,因爲線程已經不存在了,所以會捕獲異常,結束for循環
				// 銷燬構造器
				grabber.close();
				record.close();
				System.err.println("設備中斷推流成功...");
				break;
			} catch (org.bytedeco.javacv.FrameGrabber.Exception e) {
				err_index++;
			} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
				err_index++;
			}
		}
		// 程序正常結束銷燬構造器
		grabber.close();
		record.close();
		System.err.println("設備推流完畢...");
		return this;
	}
}

定時任務Timer

定時任務用來執行兩部分操作:
1.定時檢查正在推流的通道使用人數,如果該通道當前使用人數爲0,則中斷線程,結束該路視頻推流並清除緩存。
2.定時檢查正在推流的通道最後打開請求時間,如果與當前時間超過配置的保活時間時,則結束推流,並清除緩存。
當前設置的定時任務執行間隔爲1分鐘,可自行修改。

package com.junction.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import com.junction.cache.CacheUtil;
import com.junction.controller.CameraController;
import com.junction.pojo.Config;

/**
 * @Title TimerUtil.java
 * @description 定時任務
 * @time 2019年12月16日 下午3:10:08
 * @author wuguodong
 **/
@Component
public class TimerUtil implements CommandLineRunner {

	@Autowired
	private Config config;// 配置文件bean

	public static Timer timer;

	@Override
	public void run(String... args) throws Exception {
		// 超過5分鐘,結束推流
		timer = new Timer("timeTimer");
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				System.err.println("開始執行定時任務...");
				// 管理緩存
				if (null != CacheUtil.STREAMMAP && 0 != CacheUtil.STREAMMAP.size()) {
					Set<String> keys = CacheUtil.STREAMMAP.keySet();
					for (String key : keys) {
						try {
							// 最後打開時間
							long openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
									.parse(CacheUtil.STREAMMAP.get(key).getOpenTime()).getTime();
							// 當前系統時間
							long newTime = new Date().getTime();
							// 如果通道使用人數爲0,則關閉推流
							if (CacheUtil.STREAMMAP.get(key).getCount() == 0) {
								// 結束線程
								CameraController.jobMap.get(key).setInterrupted();
								// 清除緩存
								CacheUtil.STREAMMAP.remove(key);
								CameraController.jobMap.remove(key);
							} else if ((newTime - openTime) / 1000 / 60 > Integer.valueOf(config.getKeepalive())) {
								CameraController.jobMap.get(key).setInterrupted();
								CameraController.jobMap.remove(key);
								CacheUtil.STREAMMAP.remove(key);
								System.err.println("[定時任務]  關閉" + key + "攝像頭...");
							}
						} catch (ParseException e) {
							e.printStackTrace();
						}
					}
				}
				System.err.println("定時任務執行完畢...");
			}
		}, 1, 1000 * 60);
	}
}

線程池管理

package com.junction.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.junction.cache.CacheUtil;
import com.junction.controller.CameraController;
import com.junction.pojo.CameraPojo;
import com.junction.util.CameraPush;

/**
 * @Title CameraThread.java
 * @description TODO
 * @time 2019年12月16日 上午9:32:43
 * @author wuguodong
 **/
public class CameraThread {
	public static class MyRunnable implements Runnable {
		// 創建線程池
		public static ExecutorService es = Executors.newCachedThreadPool();

		private CameraPojo cameraPojo;
		private Thread nowThread;

		public MyRunnable(CameraPojo cameraPojo) {
			this.cameraPojo = cameraPojo;
		}

		// 中斷線程
		public void setInterrupted() {
			nowThread.interrupt();
		}

		@Override
		public void run() {
			// 直播流
			try {
				// 獲取當前線程存入緩存
				nowThread = Thread.currentThread();
				CacheUtil.STREAMMAP.put(cameraPojo.getToken(), cameraPojo);
				// 執行轉流推流任務
				CameraPush push = new CameraPush(cameraPojo).from();
				if (push != null) {
					push.to().go(nowThread);
				}
				// 清除緩存
				CacheUtil.STREAMMAP.remove(cameraPojo.getToken());
				CameraController.jobMap.remove(cameraPojo.getToken());
			} catch (Exception e) {
				System.err.println(
						"當前線程:" + Thread.currentThread().getName() + " 當前任務:" + cameraPojo.getRtsp() + "停止...");
				CacheUtil.STREAMMAP.remove(cameraPojo.getToken());
				CameraController.jobMap.remove(cameraPojo.getToken());
				e.printStackTrace();
			}
		}
	}
}

編寫controller

controller提供了五個接口,使用RESTful風格,故使用postman等軟件測試時,選擇相應的類型。
1.獲取視頻服務配置信息及服務運行時間
api: http://127.0.0.1:8082/camera/status (GET)
2.獲取正在推送的所有視頻流信息
api: http://127.0.0.1:8082/camera/cameras (GET)
3.開啓視頻流(直播or回放)
api: http://127.0.0.1:8082/camera/cameras (POST)
params: ip;username;password;channel;stream;starttime;endtime
4.關閉視頻流
api: http://127.0.0.1:8082/camera/cameras/:tokens (DELETE)
5.視頻流保活
api: http://127.0.0.1:8082/camera/cameras/:tokens (PUT)

1.開啓視頻流接口(POST)

先校驗參數,然後判斷緩存是否爲空(如果爲空說明目前沒有推流任務,否則遍歷緩存,通過參數判斷當前通道是否在推流。如果找到,則該路視頻的bean內人數count+1,反之調用openStream()方法進行推流)。

openStream()方法內先判斷是否存在starttime參數,如果有則說明該流爲歷史流;在判斷是否存在endtime,若無endtime則使用starttime前後各加一分鐘作爲歷史流的開始時間和結束時間。若無starttime則視爲該流爲直播流。ffmpeg在拉取rtsp直播流和歷史流時的命令不相同,所以需要上述判斷!!

通過openStream()組裝rtsp命令和rtmp命令以及UUID生成的token和其他參數,set進cameraPojo中。提交當前任務到線程池,並將當前任務線程存入jobMap(存放推流線程任務的緩存)中。

		// 執行任務
		CameraThread.MyRunnable job = new CameraThread.MyRunnable(cameraPojo);
		CameraThread.MyRunnable.es.execute(job);
		jobMap.put(token, job);

ffmpeg直播流與歷史流命令格式:
1.ffmpeg -rtsp_transport tcp -i rtsp://admin:[email protected]:554/h264/ch1/main/av_stream -vcodec h264 -f flv -an rtmp://localhost:1935/live/room
2.ffmpeg -rtsp_transport tcp -i rtsp://admin:[email protected]:554/Streaming/tracks/101?starttime=20191227t084400z’&'endtime=20191227t084600z -vcodec copy -acodec copy -f flv rtmp://localhost:1935/history/room

/**
	 * @Title: openCamera
	 * @Description: 開啓視頻流
	 * @param ip
	 * @param username
	 * @param password
	 * @param channel   通道
	 * @param stream    碼流
	 * @param starttime
	 * @param endtime
	 * @return Map<String,String>
	 **/
	@RequestMapping(value = "/cameras", method = RequestMethod.POST)
	public Map<String, String> openCamera(String ip, String username, String password, String channel, String stream,
			String starttime, String endtime) {
		// 返回結果
		Map<String, String> map = new HashMap<String, String>();
		// 校驗參數
		if (null != ip && "" != ip && null != username && "" != username && null != password && "" != password
				&& null != channel && "" != channel) {
			CameraPojo cameraPojo = new CameraPojo();
			// 獲取當前時間
			String openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime());
			Set<String> keys = CacheUtil.STREAMMAP.keySet();
			// 緩存是否爲空
			if (0 == keys.size()) {
				// 開始推流
				cameraPojo = openStream(ip, username, password, channel, stream, starttime, endtime, openTime);
				map.put("token", cameraPojo.getToken());
				map.put("url", cameraPojo.getRtmp());
			} else {
				// 是否存在的標誌;0:不存在;1:存在
				int sign = 0;
				for (String key : keys) {
					// 是否已經在推流
					if (ip.equals(CacheUtil.STREAMMAP.get(key).getIp())
							&& channel.equals(CacheUtil.STREAMMAP.get(key).getChannel())) {
						cameraPojo = CacheUtil.STREAMMAP.get(key);
						sign = 1;
						break;
					}
				}
				if (sign == 1) {
					cameraPojo.setCount(cameraPojo.getCount() + 1);
					cameraPojo.setOpenTime(openTime);
				} else {
					// 開始推流
					cameraPojo = openStream(ip, username, password, channel, stream, starttime, endtime, openTime);
				}
				map.put("token", cameraPojo.getToken());
				map.put("url", cameraPojo.getRtmp());
			}
		}

		return map;
	}

	/**
	 * @Title: openStream
	 * @Description: 推流器
	 * @param ip
	 * @param username
	 * @param password
	 * @param channel
	 * @param stream
	 * @param starttime
	 * @param endtime
	 * @param openTime
	 * @return
	 * @return CameraPojo
	 **/
	private CameraPojo openStream(String ip, String username, String password, String channel, String stream,
			String starttime, String endtime, String openTime) {
		CameraPojo cameraPojo = new CameraPojo();
		// 生成token
		String token = UUID.randomUUID().toString();
		String rtsp = "";
		String rtmp = "";
		// 歷史流
		if (null != starttime && "" != starttime) {
			if (null != endtime && "" != endtime) {
				rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/Streaming/tracks/" + channel
						+ "01?starttime=" + starttime.substring(0, 8) + "t" + starttime.substring(8) + "z'&'endtime="
						+ endtime.substring(0, 8) + "t" + endtime.substring(8) + "z";
			} else {
				try {
					SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
					String startTime = df.format(df.parse(starttime).getTime() - 60 * 1000);
					String endTime = df.format(df.parse(starttime).getTime() + 60 * 1000);
					rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/Streaming/tracks/" + channel
							+ "01?starttime=" + startTime.substring(0, 8) + "t" + startTime.substring(8)
							+ "z'&'endtime=" + endTime.substring(0, 8) + "t" + endTime.substring(8) + "z";
				} catch (ParseException e) {
					e.printStackTrace();
				}
			}
			rtmp = "rtmp://" + config.getPush_ip() + ":" + config.getPush_port() + "/history/" + token;
		} else {// 直播流
			rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/h264/ch" + channel + "/" + stream
					+ "/av_stream";
			rtmp = "rtmp://" + config.getPush_ip() + ":" + config.getPush_port() + "/live/" + token;
		}

		cameraPojo.setUsername(username);
		cameraPojo.setPassword(password);
		cameraPojo.setIp(ip);
		cameraPojo.setChannel(channel);
		cameraPojo.setStream(stream);
		cameraPojo.setRtsp(rtsp);
		cameraPojo.setRtmp(rtmp);
		cameraPojo.setOpenTime(openTime);
		cameraPojo.setCount(1);
		cameraPojo.setToken(token);

		// 執行任務
		CameraThread.MyRunnable job = new CameraThread.MyRunnable(cameraPojo);
		CameraThread.MyRunnable.es.execute(job);
		jobMap.put(token, job);

		return cameraPojo;
	}

2.關閉視頻流接口(DELETE)

傳入參數爲tokens,通過,分隔,可以同時關閉多路視頻。通過token查找緩存判斷是否存在,如果存在,則人數count-1。不直接調用結束線程的方法是爲了滿足如果多個客戶端同時觀看該路視頻,一人關閉會影響其他人使用。故調用該接口只是使該路視頻的使用人數-1,最終結束線程的操作交由定時任務處理,如果定時器查詢到視頻使用人數的count爲0,則結束該路視頻的推流操作,並清除緩存。

/**
	 * @Title: closeCamera
	 * @Description:關閉視頻流
	 * @param tokens
	 * @return void
	 **/
	@RequestMapping(value = "/cameras/{tokens}", method = RequestMethod.DELETE)
	public void closeCamera(@PathVariable("tokens") String tokens) {
		if (null != tokens && "" != tokens) {
			String[] tokenArr = tokens.split(",");
			for (String token : tokenArr) {
				if (jobMap.containsKey(token) && CacheUtil.STREAMMAP.containsKey(token)) {
					if (0 < CacheUtil.STREAMMAP.get(token).getCount()) {
						// 人數-1
						CacheUtil.STREAMMAP.get(token).setCount(CacheUtil.STREAMMAP.get(token).getCount() - 1);
					}
				}
			}
		}
	}

3.獲取視頻流(GET)

獲取當前進行的推流任務。

/**
	 * @Title: getCameras
	 * @Description:獲取視頻流
	 * @return Map<String, CameraPojo>
	 **/
	@RequestMapping(value = "/cameras", method = RequestMethod.GET)
	public Map<String, CameraPojo> getCameras() {
		return CacheUtil.STREAMMAP;
	}

4.視頻流保活(PUT)

視頻流保活的作用是爲了應付以下場景:
如果客戶端比如瀏覽器直接關閉掉,並不會通知服務客戶已經不再觀看視頻了,這是服務還在進行推流。所以添加保活機制,如果客戶端沒有觸發保活機制,定時任務執行時,如果該路視頻的最後打開時間距當前時間超過配置的保活時間時,關閉該路視頻的推流任務。如果客戶端觸發保活機制時,更新該路視頻的最後打開時間(opentime)爲當前系統時間。

/**
	 * @Title: keepAlive
	 * @Description:視頻流保活
	 * @param tokens
	 * @return void
	 **/
	@RequestMapping(value = "/cameras/{tokens}", method = RequestMethod.PUT)
	public void keepAlive(@PathVariable("tokens") String tokens) {
		// 校驗參數
		if (null != tokens && "" != tokens) {
			String[] tokenArr = tokens.split(",");
			for (String token : tokenArr) {
				CameraPojo cameraPojo = new CameraPojo();
				// 直播流token
				if (null != CacheUtil.STREAMMAP.get(token)) {
					cameraPojo = CacheUtil.STREAMMAP.get(token);
					// 更新當前系統時間
					cameraPojo.setOpenTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime()));
				}
			}
		}
	}

5.獲取服務信息(GET)

通過該接口獲取服務運行時間,以及配置文件的配置

	/**
	 * @Title: getConfig
	 * @Description: 獲取服務信息
	 * @return Map<String, Object>
	 **/
	@RequestMapping(value = "/status", method = RequestMethod.GET)
	public Map<String, Object> getConfig() {
		// 獲取當前時間
		long nowTime = new Date().getTime();
		String upTime = (nowTime - CacheUtil.STARTTIME) / (1000 * 60 * 60) + "時"
				+ (nowTime - CacheUtil.STARTTIME) % (1000 * 60 * 60) / (1000 * 60) + "分";
		Map<String, Object> status = new HashMap<String, Object>();
		status.put("config", config);
		status.put("uptime", upTime);
		return status;
	}

6.video.js

測試需要的video.js。video.js用來播放rtmp的視頻。注意chrome需要先允許加載flash插件(百度一下很簡單的)。使用以下代碼,在src處添加推流成功的rtmp地址。

<!DOCTYPE html>
<html lang="en">
<head>
<title>Video.js | HTML5 Video Player</title>
<link href="http://vjs.zencdn.net/5.20.1/video-js.css" rel="stylesheet">
<script src="http://vjs.zencdn.net/5.20.1/videojs-ie8.min.js"></script>
</head>
<body width="640px" height="360px">

	<video id="example_video_1" class="video-js vjs-default-skin" controls
		preload="auto" width="640px" height="360px" data-setup="{}"
		style="float: left">
		<source src="此處填入rtmp地址" type="rtmp/flv">
		<p class="vjs-no-js">
			To view this video please enable JavaScript, and consider upgrading
			to a web browser that <a
				href="http://videojs.com/html5-video-support/" target="_blank">supports
				HTML5 video</a>
		</p>
	</video>
</body>
</html>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章