这里就会获取到实时流和历史流的rtsp地址 后续文章会使用这个地址实现视频推流
Java 视频直播JavaCV(ffmpeg h264)+RTSP实现低延时1秒推流
吐槽下可搜索的资源真的是少,好多都是4 5年前的资料
onvif通用协议就不说了,大多摄像头都可用,利用onvif获取实时流和历史流转换成h264编码推送到前端,用h5新标签直接播放
onvif协议大部分是c或者android 资料 java的比较少 感谢大佬留下的宝贵资源:
1. https://github.com/RootSoft/ONVIF-Java (功能不够完善,没有使用例子)
2. https://github.com/fpompermaier/onvif (这个是基于上面的项目封装,有简单的例子,这里使用的这个)
实际使用:
前言:因为例子有限,没有历史流rtsp地址的使用,在参考例子和源码下,走了很多坑
后续更新利用javacv 使用rtsp地址拉取流推送出去。
上代码:
重点!重点!重点!总要的地方说三遍,将上述的第二个github项目拉下来打包到本地仓库 下面的
pom 中的onvif-ws-client就是打包到本地仓库的
项目地址:https://github.com/xsjzf/onvif_java
核心的地方是:
这里说明下例子中只有实时流rtsp地址的方法获取
在onvif-ws-client的源码中有很多功能例子中只有media 也就是实时流的
要使用历史流则需要自行添加 就是replay和recording 其他功能详见onvif的官网文档
Capabilities capabilities = this.device.getCapabilities(Arrays.asList(CapabilityCategory.ALL));
@javax.xml.bind.annotation.XmlElement(name = "Analytics")
protected org.onvif.ver10.schema.AnalyticsCapabilities analytics;
@javax.xml.bind.annotation.XmlElement(name = "Device")
protected org.onvif.ver10.schema.DeviceCapabilities device;
@javax.xml.bind.annotation.XmlElement(name = "Events")
protected org.onvif.ver10.schema.EventCapabilities events;
@javax.xml.bind.annotation.XmlElement(name = "Imaging")
protected org.onvif.ver10.schema.ImagingCapabilities imaging;
@javax.xml.bind.annotation.XmlElement(name = "Media")
protected org.onvif.ver10.schema.MediaCapabilities media;
@javax.xml.bind.annotation.XmlElement(name = "PTZ")
protected org.onvif.ver10.schema.PTZCapabilities ptz;
@javax.xml.bind.annotation.XmlElement(name = "Extension")
protected org.onvif.ver10.schema.CapabilitiesExtension extension;
若需要历史回放功能呢,则需要对上面第二个github 地址进行修改,修改如下:OnvifDevice 的init方法中
Capabilities对象里面除了基本的功能大多在Extension里面
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "CapabilitiesExtension", propOrder = {
"any",
"deviceIO",
"display",
"recording",
"search",
"replay",
"receiver",
"analyticsDevice",
"extensions"
})
public class CapabilitiesExtension {
@XmlAnyElement(lax = true)
protected List<java.lang.Object> any;
@XmlElement(name = "DeviceIO")
protected DeviceIOCapabilities deviceIO;
@XmlElement(name = "Display")
protected DisplayCapabilities display;
@XmlElement(name = "Recording")
protected RecordingCapabilities recording;
@XmlElement(name = "Search")
protected SearchCapabilities search;
@XmlElement(name = "Replay")
protected ReplayCapabilities replay;
@XmlElement(name = "Receiver")
protected ReceiverCapabilities receiver;
@XmlElement(name = "AnalyticsDevice")
protected AnalyticsDeviceCapabilities analyticsDevice;
@XmlElement(name = "Extensions")
protected CapabilitiesExtension2 extensions;
例如我这里需要历史流的功能那么我在init方法中初始化添加上
if (capabilities.getExtension().getReplay() != null && capabilities.getExtension().getReplay().getXAddr() != null) {
this.replayPort = new ReplayService().getReplayPort();
this.replayPort =
getServiceProxy((BindingProvider) replayPort, capabilities.getExtension().getReplay().getXAddr())
.create(ReplayPort.class);
}
if (capabilities.getExtension().getRecording() != null && capabilities.getExtension().getRecording().getXAddr() != null) {
this.recordingPort = new RecordingService().getRecordingPort();
this.recordingPort =
getServiceProxy((BindingProvider) recordingPort, capabilities.getExtension().getRecording().getXAddr())
.create(RecordingPort.class);
}
if (capabilities.getExtension().getSearch() != null && capabilities.getExtension().getSearch().getXAddr() != null) {
this.searchPort = new SearchService().getSearchPort();
this.searchPort =
getServiceProxy((BindingProvider) searchPort, capabilities.getExtension().getSearch().getXAddr())
.create(SearchPort.class);
}
具体使用:
/**
* 获取到OnvifDevice对象
* @param host 摄像头地址 92.168.xx.yy, or http://host[:port]
* @param username 用户名
* @param password 密码
* @param profileToken "MediaProfile000" If empty, will use first profile.
* @return
*/
public static OnvifDevice getOnvifCredentials(String host, String username, String password, String profileToken){
try {
OnvifCredentials credentials = new OnvifCredentials(host, username, password, profileToken);
//补全host
URL u = credentials.getHost().startsWith("http")
? new URL(credentials.getHost())
: new URL("http://" + credentials.getHost());
return new OnvifDevice(u, credentials.getUser(), credentials.getPassword());
} catch (MalformedURLException | ConnectException | SOAPException e) {
e.printStackTrace();
throw new RrException(e.getMessage());
}
}
/**
* 获取实时rtsp地址
* @param onvifDevice 设备
* @return
* @throws Exception
*/
public static String getRtspUrl(OnvifDevice onvifDevice) throws Exception {
List<Profile> profiles = onvifDevice.getMedia().getProfiles();
for (Profile profile : profiles) {
String profileToken = profile.getToken();
String rtsp = onvifDevice.getStreamUri(profileToken, TransportProtocol.RTSP);
String uri = "rtsp://" + onvifDevice.getUser() + ":" + onvifDevice.getPassword() + "@" + rtsp.replace("rtsp://", "");
}
return "";
}
/**
* 获取历史rtsp地址
* @param onvifDevice 设备
* @return
*/
public static String getReplayUrl(OnvifDevice onvifDevice){
List<GetRecordingsResponseItem> recordings = onvifDevice.getRecordingPort().getRecordings();
for (GetRecordingsResponseItem recording : recordings) {
String recordingToken = recording.getRecordingToken();
String rtsp = onvifDevice.getReplayUri(onvifDevice, recordingToken, TransportProtocol.RTSP);
String uri = "rtsp://" + onvifDevice.getUser() + ":" + onvifDevice.getPassword() + "@" + rtsp.replace("rtsp://", "");
}
return "";
}
/**
* 转换 时间段为rtsp时间格式
* @param start 开始
* @param end 结束
* @return ?starttime=20200908t093812z&endtime=20200908t104816z
*/
public static String getRtspTimeSpace(LocalDateTime start, LocalDateTime end){
Long st = start.toInstant(ZoneOffset.of("+8")).toEpochMilli();
Long ed = end.toInstant(ZoneOffset.of("+8")).toEpochMilli();
String ios8601St = getDate(st);
String ios8601Ed = getDate(st);
return "?starttime=" + ios8601St + "&endtime=" + ios8601Ed;
}
/**
* 时间戳转换成IOS8601格式
* @param beginTime
* @return
*/
public static String getDate(Long beginTime) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String bt = format.format(beginTime);
Date date = null;
try {
date = format.parse(bt);
} catch (Exception e) {
e.printStackTrace();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'z'");
return sdf.format(date);
}
2020-11-27:
为
兼容海康,大华和宇视,修改相关rtsp组装
/**
* 获取实时rtsp地址
* 海康nvr:
* (主码流)rtsp://**.168.101.**:554/Streaming/Unicast/channels/101
* (子码流)rtsp://**.168.101.**:554/Streaming/Unicast/channels/102
* 海康单机:
* (主码流)rtsp://**.168.30.**:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1
* (子码流)rtsp://**.168.30.**:554/Streaming/Channels/102?transportmode=unicast&profile=Profile_2
* 大华单机:
* (主码流)rtsp://**.168.30.**:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
* (子码流)rtsp://**.168.30.**:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
* @param onvifDevice 设备
* @return
* @throws Exception
*/
public static Map<String, String> getRtspUrl(OnvifDevice onvifDevice, Integer id) {
Map<String, String> map = Maps.newLinkedHashMap();
//兼容大华设备
List<Profile> profiles = onvifDevice.getMedia().getProfiles();
if (profiles == null || profiles.size() == 0){
LOG.error("未查询到实时Profiles");
return map;
}
for (Profile profile : profiles) {
String profileToken = profile.getToken();
System.out.println(profileToken);
String rtsp = onvifDevice.getStreamUri(profileToken, TransportProtocol.RTSP);
String uri = "rtsp://" + onvifDevice.getUser() + ":" + onvifDevice.getPassword() + "@" + rtsp.replace("rtsp://", "");
//只取主码流 01结尾
//兼容单个摄像头无法从profileToken获取到通道号
//兼容大华单个摄像头
String channel = id + profileToken;
// String channel = profileToken.substring(profileToken.length() - 3);
String hkUrl = rtsp.contains("?") ? rtsp.substring(0, rtsp.indexOf("?")) : rtsp;
if (hkUrl.endsWith("01") || rtsp.contains("subtype=0")){
map.put(channel, uri);
}
}
return map;
}
/**
* 获取历史rtsp地址
* rtsp://admin:zouwei678@192.168.101.2:554/Streaming/tracks/301?starttime=20201119T134258z&endtime=20201119T134316z
* rtsp://admin:zouwei678@192.168.101.2:554/Streaming/Unicast/channels/301?starttime=20201119T134258z&endtime=20201119T134316z
* @param onvifDevice 设备
* @return
*/
public static Map<String, String> getReplayUrl(OnvifDevice onvifDevice, Integer id){
Map<String, String> map = Maps.newLinkedHashMap();
RecordingPort recordingPort = onvifDevice.getRecordingPort();
if (recordingPort == null){
LOG.error("该设备不支持历史回放");
return map;
}
List<GetRecordingsResponseItem> recordings = recordingPort.getRecordings();
if (recordings == null || recordings.size() == 0){
LOG.error("未查询到历史Profiles");
return map;
}
for (GetRecordingsResponseItem recording : recordings) {
String recordingToken = recording.getRecordingToken();
// String channel = recordingToken.substring(recordingToken.length() - 3);
String channel = id + recordingToken;
String rtsp = onvifDevice.getReplayUri(onvifDevice, recordingToken, TransportProtocol.RTSP);
String uri = "rtsp://" + onvifDevice.getUser() + ":" + onvifDevice.getPassword() + "@" + rtsp.replace("rtsp://", "");
map.put(channel, uri);
}
return map;
}
在实际项目中使用发现,海康的nvr录像机在使用rtsp拼接时间方式获取历史流中,同一时间只能开启一路,再开启时会报错
这个错误经过多方搜索并不是ffmpeg或者javacv的问题,直接使用vlc打开也是这样的,根本原因是带宽限制,目前没有解决方法,有大佬有方法的麻烦告知下,不胜感激
****持续更新****
2021-04-12 介于很多人导入
<dependency>
<groupId>org.onvif</groupId>
<artifactId>onvif-ws-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在此提供直接放入本地仓库包
提取码:v974
直接解压到本地仓库org文件包下
实际使用封装:
/**
* 初始化Onvif
* @param ip
* @param userName
* @param passwd
* @param id
*/
private void initOnvif(String ip, String userName, String passwd, Integer id) {
try {
OnvifDevice onvifDevice = MediaUtils.getOnvifCredentials(ip, userName, passwd, "MediaProfile000");
Map<String, String> rtspUrl = MediaUtils.getRtspUrl(onvifDevice, id);
Map<String, String> replayUrl = MediaUtils.getReplayUrl(onvifDevice, id);
if (rtspUrl.isEmpty()){
LOG.error("初始化加载未获取到实时RTSP地址: " + ip);
}
if (replayUrl.isEmpty()){
LOG.error("初始化加载未获取到历史RTSP地址: " + ip);
}
map.put(id, new CameraInfo(rtspUrl, replayUrl));
System.out.println("加载成功服务:" + ip);
} catch (Exception e) {
e.printStackTrace();
System.out.println("加载失败:" + ip);
}
}
package com.onvif.java.utils;
import com.google.common.collect.Maps;
import com.onvif.java.model.OnvifCredentials;
import com.onvif.java.service.OnvifDevice;
import com.onvif.java.service.UdpService;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.onvif.ver10.recording.wsdl.RecordingPort;
import org.onvif.ver10.schema.GetRecordingsResponseItem;
import org.onvif.ver10.schema.Profile;
import org.onvif.ver10.schema.TransportProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;
/**
* @program: javaOnvif
* @description: 获取rtsp地址
* @author: zf
* @create: 2020-09-08 10:50
**/
@Service
public class MediaUtils {
/**
* 获取到OnvifDevice对象
* @param host 摄像头地址 92.168.xx.yy, or http://host[:port]
* @param username 用户名
* @param password 密码
* @param profileToken "MediaProfile000" If empty, will use first profile.
* @return
*/
public static OnvifDevice getOnvifCredentials(String host, String username, String password, String profileToken) throws Exception {
OnvifCredentials credentials = new OnvifCredentials(host, username, password, profileToken);
//补全host
URL u = credentials.getHost().startsWith("http")
? new URL(credentials.getHost())
: new URL("http://" + credentials.getHost());
return new OnvifDevice(u, credentials.getUser(), credentials.getPassword());
}
/**
* 获取实时rtsp地址
* 海康nvr:
* (主码流)rtsp://**.168.101.**:554/Streaming/Unicast/channels/101
* (子码流)rtsp://**.168.101.**:554/Streaming/Unicast/channels/102
* 海康单机:
* (主码流)rtsp://**.168.30.**:554/Streaming/Channels/101?transportmode=unicast&profile=Profile_1
* (子码流)rtsp://**.168.30.**:554/Streaming/Channels/102?transportmode=unicast&profile=Profile_2
* 大华单机:
* (主码流)rtsp://**.168.30.**:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
* (子码流)rtsp://**.168.30.**:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif
* @param onvifDevice 设备
* @return
* @throws Exception
*/
public static void getRtspUrl(OnvifDevice onvifDevice, Integer id) {
Map<String, String> map = Maps.newLinkedHashMap();
//兼容大华设备
List<Profile> profiles = onvifDevice.getMedia().getProfiles();
if (profiles == null || profiles.size() == 0){
return;
}
for (Profile profile : profiles) {
String profileToken = profile.getToken();
String rtsp = onvifDevice.getStreamUri(profileToken, TransportProtocol.RTSP);
String uri = "rtsp://" + onvifDevice.getUser() + ":" + onvifDevice.getPassword() + "@" + rtsp.replace("rtsp://", "");
System.out.println(uri);
}
}
/**
* 获取历史rtsp地址
* rtsp://admin:zouwei678@192.168.101.2:554/Streaming/tracks/301?starttime=20201119T134258z&endtime=20201119T134316z
* rtsp://admin:zouwei678@192.168.101.2:554/Streaming/Unicast/channels/301?starttime=20201119T134258z&endtime=20201119T134316z
* @param onvifDevice 设备
* @return
*/
public static void getReplayUrl(OnvifDevice onvifDevice, Integer id){
Map<String, String> map = Maps.newLinkedHashMap();
RecordingPort recordingPort = onvifDevice.getRecordingPort();
if (recordingPort == null){
return;
}
List<GetRecordingsResponseItem> recordings = recordingPort.getRecordings();
if (recordings == null || recordings.size() == 0){
return;
}
for (GetRecordingsResponseItem recording : recordings) {
String recordingToken = recording.getRecordingToken();
// String channel = recordingToken.substring(recordingToken.length() - 3);
String channel = id + recordingToken;
String rtsp = onvifDevice.getReplayUri(onvifDevice, recordingToken, TransportProtocol.RTSP);
String uri = "rtsp://" + onvifDevice.getUser() + ":" + onvifDevice.getPassword() + "@" + rtsp.replace("rtsp://", "");
System.out.println(uri);
}
}
/**
* 转换 时间段为rtsp时间格式
* @param st 开始
* @param end 结束
* @return ?starttime=20200908t093812z&endtime=20200908t104816z
*/
public static String getRtspTimeSpace(Long st, Long end){
String ios8601St = getDate(st);
String ios8601Ed = getDate(end);
return "?starttime=" + ios8601St + "&endtime=" + ios8601Ed;
}
/**
* 时间戳转换成IOS8601格式
* @param beginTime
* @return
*/
public static String getDate(Long beginTime) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String bt = format.format(beginTime);
Date date = null;
try {
date = format.parse(bt);
} catch (Exception e) {
e.printStackTrace();
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'z'");
return sdf.format(date);
}
}
package com.onvif.java.model;
import lombok.Data;
/**
* onvif 连接信息
* @author zf
*/
@Data
public class OnvifCredentials {
/**
* 92.168.xx.yy, or http://host[:port]
*/
private String host;
/**
* admin
*/
private String user;
/**
* secret
*/
private String password;
/**
* "MediaProfile000" If empty, will use first profile.
*/
private String profile;
public OnvifCredentials(String host, String user, String password, String profile) {
this.host = host;
this.user = user;
this.password = password;
this.profile = profile;
}
public String details() {
return host + "," + user + "," + password + "," + profile;
}
}