first commit
This commit is contained in:
commit
cae710b86b
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
README.md
|
||||||
|
target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
224
pom.xml
Normal file
224
pom.xml
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>io.github.javpower</groupId>
|
||||||
|
<artifactId>rtsp-converter-flv-spring-boot-starter</artifactId>
|
||||||
|
<version>1.5.9</version>
|
||||||
|
<name>rtsp-converter-flv-spring-boot-starter</name>
|
||||||
|
<description>a tool about easy rtsp-converter-flv</description>
|
||||||
|
<url>https://github.com/javpower/easy-flv</url>
|
||||||
|
<licenses>
|
||||||
|
<license>
|
||||||
|
<name> The Apache Software License, Version 2.0 </name>
|
||||||
|
<url> http://www.apache.org/licenses/LICENSE-2.0.txt </url>
|
||||||
|
<distribution> repo </distribution>
|
||||||
|
</license>
|
||||||
|
</licenses>
|
||||||
|
<scm>
|
||||||
|
<url>https://github.com/javpower/easy-flv</url>
|
||||||
|
<connection>scm:git@github.com/javpower/easy-flv.git</connection>
|
||||||
|
<developerConnection>scm:git@github.com/javpower/easy-flv.git</developerConnection>
|
||||||
|
</scm>
|
||||||
|
<developers>
|
||||||
|
<developer>
|
||||||
|
<name>gc.x</name>
|
||||||
|
<email>javpower@163.com</email>
|
||||||
|
<organization> https://github.com/javpower</organization>
|
||||||
|
<timezone>+8</timezone>
|
||||||
|
</developer>
|
||||||
|
</developers>
|
||||||
|
<properties>
|
||||||
|
<project.version>0.0.1-SNAPSHOT</project.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
<java.version>1.8</java.version>
|
||||||
|
<spring-boot.version>2.7.13</spring-boot.version>
|
||||||
|
<mica-auto.vaersion>2.3.2</mica-auto.vaersion>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-log4j2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.lmax</groupId>
|
||||||
|
<artifactId>disruptor</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
<version>1.2.31</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacv-platform</artifactId>
|
||||||
|
<version>1.5.9</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Additional dependencies required to use CUDA and cuDNN -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv-platform-gpu</artifactId>
|
||||||
|
<version>4.7.0-1.5.9</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Optional GPL builds with (almost) everything enabled -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg-platform-gpl</artifactId>
|
||||||
|
<version>6.0-1.5.9</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.dreamlu</groupId>
|
||||||
|
<artifactId>mica-auto</artifactId>
|
||||||
|
<version>${mica-auto.vaersion}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<distributionManagement>
|
||||||
|
<snapshotRepository>
|
||||||
|
<id>ossrh</id>
|
||||||
|
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
|
||||||
|
</snapshotRepository>
|
||||||
|
<repository>
|
||||||
|
<id>ossrh</id>
|
||||||
|
<url> https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||||
|
</repository>
|
||||||
|
</distributionManagement>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.gc.easy.EasyHttpApplication</mainClass>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.sonatype.plugins</groupId>
|
||||||
|
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||||
|
<version>1.6.7</version>
|
||||||
|
<extensions>true</extensions>
|
||||||
|
<configuration>
|
||||||
|
<serverId>ossrh</serverId>
|
||||||
|
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
|
||||||
|
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId> org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-source-plugin</artifactId>
|
||||||
|
<version>2.2.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>attach-sources</id>
|
||||||
|
<goals>
|
||||||
|
<goal>jar-no-fork</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
|
<version>2.9.1</version>
|
||||||
|
<configuration>
|
||||||
|
<show>private</show>
|
||||||
|
<nohelp>true</nohelp>
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
<docencoding>UTF-8</docencoding>
|
||||||
|
<additionalparam>-Xdoclint:none</additionalparam>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-gpg-plugin</artifactId>
|
||||||
|
<version>1.5</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>sign-artifacts</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>sign</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
|
<version>2.8.2</version>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
15
src/main/java/com/gc/easy/flv/config/FlvConfig.java
Normal file
15
src/main/java/com/gc/easy/flv/config/FlvConfig.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package com.gc.easy.flv.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "easy.flv")
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
public class FlvConfig {
|
||||||
|
private String host;
|
||||||
|
private Integer wight=1920;
|
||||||
|
private Integer height=1080;
|
||||||
|
|
||||||
|
}
|
35
src/main/java/com/gc/easy/flv/controller/FLVController.java
Normal file
35
src/main/java/com/gc/easy/flv/controller/FLVController.java
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package com.gc.easy.flv.controller;
|
||||||
|
|
||||||
|
import com.gc.easy.flv.service.IFLVService;
|
||||||
|
import com.gc.easy.flv.service.IOpenFLVService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FLV流转换
|
||||||
|
*
|
||||||
|
* @author gc.x
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
public class FLVController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IFLVService service;
|
||||||
|
@Autowired(required = false)
|
||||||
|
private IOpenFLVService openFLVService;
|
||||||
|
|
||||||
|
@GetMapping(value = "/get/flv/hls/stream_{channel}.flv")
|
||||||
|
public void open(@PathVariable(value = "channel") Integer channel, HttpServletResponse response,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
String url = openFLVService.getUrl(channel);
|
||||||
|
if(!StringUtils.isEmpty(url)){
|
||||||
|
service.open(url, response, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package com.gc.easy.flv.controller;
|
||||||
|
|
||||||
|
import com.gc.easy.flv.config.FlvConfig;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FLV流转换
|
||||||
|
*
|
||||||
|
* @author gc.x
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
public class FLVPlayController {
|
||||||
|
@Autowired
|
||||||
|
private FlvConfig flvConfig;
|
||||||
|
@GetMapping(value = "/flv/hls/stream_{channel}.flv")
|
||||||
|
public String getAppHtml(@PathVariable(value = "channel") Integer channel, Model model) {
|
||||||
|
String videoPath=flvConfig.getHost()+"/get/flv/hls/stream_"+channel+".flv";
|
||||||
|
model.addAttribute("videoPath", videoPath);
|
||||||
|
model.addAttribute("wight", flvConfig.getWight());
|
||||||
|
model.addAttribute("height", flvConfig.getHeight());
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
}
|
37
src/main/java/com/gc/easy/flv/factories/Converter.java
Normal file
37
src/main/java/com/gc/easy/flv/factories/Converter.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package com.gc.easy.flv.factories;
|
||||||
|
|
||||||
|
import javax.servlet.AsyncContext;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface Converter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取该转换的key
|
||||||
|
*/
|
||||||
|
public String getKey();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取该转换的url
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个流输出
|
||||||
|
*
|
||||||
|
* @param entity
|
||||||
|
*/
|
||||||
|
public void addOutputStreamEntity(String key, AsyncContext entity) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出转换
|
||||||
|
*/
|
||||||
|
public void exit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动
|
||||||
|
*/
|
||||||
|
public void start();
|
||||||
|
|
||||||
|
}
|
227
src/main/java/com/gc/easy/flv/factories/ConverterFactories.java
Normal file
227
src/main/java/com/gc/easy/flv/factories/ConverterFactories.java
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package com.gc.easy.flv.factories;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.util.IOUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bytedeco.ffmpeg.avcodec.AVPacket;
|
||||||
|
import org.bytedeco.ffmpeg.global.avcodec;
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||||
|
|
||||||
|
import javax.servlet.AsyncContext;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* javacv转包装<br/>
|
||||||
|
* 无须转码,更低的资源消耗,更低的延迟<br/>
|
||||||
|
* 确保流来源视频H264格式,音频AAC格式
|
||||||
|
*
|
||||||
|
* @author gc.x
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class ConverterFactories extends Thread implements Converter {
|
||||||
|
public volatile boolean runing = true;
|
||||||
|
/**
|
||||||
|
* 读流器
|
||||||
|
*/
|
||||||
|
private FFmpegFrameGrabber grabber;
|
||||||
|
/**
|
||||||
|
* 转码器
|
||||||
|
*/
|
||||||
|
private FFmpegFrameRecorder recorder;
|
||||||
|
/**
|
||||||
|
* 转FLV格式的头信息<br/>
|
||||||
|
* 如果有第二个客户端播放首先要返回头信息
|
||||||
|
*/
|
||||||
|
private byte[] headers;
|
||||||
|
/**
|
||||||
|
* 保存转换好的流
|
||||||
|
*/
|
||||||
|
private ByteArrayOutputStream stream;
|
||||||
|
/**
|
||||||
|
* 流地址,h264,aac
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
/**
|
||||||
|
* 流输出
|
||||||
|
*/
|
||||||
|
private List<AsyncContext> outEntitys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key用于表示这个转换器
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换队列
|
||||||
|
*/
|
||||||
|
private Map<String, Converter> factories;
|
||||||
|
|
||||||
|
public ConverterFactories(String url, String key, Map<String, Converter> factories, List<AsyncContext> outEntitys) {
|
||||||
|
this.url = url;
|
||||||
|
this.key = key;
|
||||||
|
this.factories = factories;
|
||||||
|
this.outEntitys = outEntitys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean isCloseGrabberAndResponse = true;
|
||||||
|
try {
|
||||||
|
grabber = new FFmpegFrameGrabber(url);
|
||||||
|
if ("rtsp".equals(url.substring(0, 4))) {
|
||||||
|
grabber.setOption("rtsp_transport", "tcp");
|
||||||
|
grabber.setOption("stimeout", "5000000");
|
||||||
|
}
|
||||||
|
grabber.start();
|
||||||
|
if (avcodec.AV_CODEC_ID_H264 == grabber.getVideoCodec()
|
||||||
|
&& (grabber.getAudioChannels() == 0 || avcodec.AV_CODEC_ID_AAC == grabber.getAudioCodec())) {
|
||||||
|
log.info("this url:{} converterFactories start", url);
|
||||||
|
// 来源视频H264格式,音频AAC格式
|
||||||
|
// 无须转码,更低的资源消耗,更低的延迟
|
||||||
|
stream = new ByteArrayOutputStream();
|
||||||
|
recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(),
|
||||||
|
grabber.getAudioChannels());
|
||||||
|
recorder.setInterleaved(true);
|
||||||
|
recorder.setVideoOption("preset", "ultrafast");
|
||||||
|
recorder.setVideoOption("tune", "zerolatency");
|
||||||
|
recorder.setVideoOption("crf", "25");
|
||||||
|
recorder.setFrameRate(grabber.getFrameRate());
|
||||||
|
recorder.setSampleRate(grabber.getSampleRate());
|
||||||
|
if (grabber.getAudioChannels() > 0) {
|
||||||
|
recorder.setAudioChannels(grabber.getAudioChannels());
|
||||||
|
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
||||||
|
recorder.setAudioCodec(grabber.getAudioCodec());
|
||||||
|
}
|
||||||
|
recorder.setFormat("flv");
|
||||||
|
recorder.setVideoBitrate(grabber.getVideoBitrate());
|
||||||
|
recorder.setVideoCodec(grabber.getVideoCodec());
|
||||||
|
recorder.start(grabber.getFormatContext());
|
||||||
|
if (headers == null) {
|
||||||
|
headers = stream.toByteArray();
|
||||||
|
stream.reset();
|
||||||
|
writeResponse(headers);
|
||||||
|
}
|
||||||
|
int nullNumber = 0;
|
||||||
|
while (runing) {
|
||||||
|
AVPacket k = grabber.grabPacket();
|
||||||
|
if (k != null) {
|
||||||
|
try {
|
||||||
|
recorder.recordPacket(k);
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
if (stream.size() > 0) {
|
||||||
|
byte[] b = stream.toByteArray();
|
||||||
|
stream.reset();
|
||||||
|
writeResponse(b);
|
||||||
|
if (outEntitys.isEmpty()) {
|
||||||
|
log.info("没有输出退出");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avcodec.av_packet_unref(k);
|
||||||
|
} else {
|
||||||
|
nullNumber++;
|
||||||
|
if (nullNumber > 200) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.sleep(5);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isCloseGrabberAndResponse = false;
|
||||||
|
// 需要转码为视频H264格式,音频AAC格式
|
||||||
|
ConverterTranFactories c = new ConverterTranFactories(url, key, factories, outEntitys, grabber);
|
||||||
|
factories.put(key, c);
|
||||||
|
c.start();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
closeConverter(isCloseGrabberAndResponse);
|
||||||
|
completeResponse(isCloseGrabberAndResponse);
|
||||||
|
log.info("this url:{} converterFactories exit", url);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出FLV视频流
|
||||||
|
*
|
||||||
|
* @param b
|
||||||
|
*/
|
||||||
|
public void writeResponse(byte[] b) {
|
||||||
|
Iterator<AsyncContext> it = outEntitys.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
AsyncContext o = it.next();
|
||||||
|
try {
|
||||||
|
o.getResponse().getOutputStream().write(b);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("移除一个输出");
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出转换
|
||||||
|
*/
|
||||||
|
public void closeConverter(boolean isCloseGrabberAndResponse) {
|
||||||
|
if (isCloseGrabberAndResponse) {
|
||||||
|
IOUtils.close(grabber);
|
||||||
|
factories.remove(this.key);
|
||||||
|
}
|
||||||
|
IOUtils.close(recorder);
|
||||||
|
IOUtils.close(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭异步响应
|
||||||
|
*
|
||||||
|
* @param isCloseGrabberAndResponse
|
||||||
|
*/
|
||||||
|
public void completeResponse(boolean isCloseGrabberAndResponse) {
|
||||||
|
if (isCloseGrabberAndResponse) {
|
||||||
|
Iterator<AsyncContext> it = outEntitys.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
AsyncContext o = it.next();
|
||||||
|
o.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKey() {
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addOutputStreamEntity(String key, AsyncContext entity) throws IOException {
|
||||||
|
if (headers == null) {
|
||||||
|
outEntitys.add(entity);
|
||||||
|
} else {
|
||||||
|
entity.getResponse().getOutputStream().write(headers);
|
||||||
|
entity.getResponse().getOutputStream().flush();
|
||||||
|
outEntitys.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exit() {
|
||||||
|
this.runing = false;
|
||||||
|
try {
|
||||||
|
this.join();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,212 @@
|
|||||||
|
package com.gc.easy.flv.factories;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.util.IOUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.bytedeco.ffmpeg.global.avcodec;
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||||
|
import org.bytedeco.javacv.Frame;
|
||||||
|
|
||||||
|
import javax.servlet.AsyncContext;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* javacv转码<br/>
|
||||||
|
* 流来源不是视频H264格式,音频AAC格式 转码为视频H264格式,音频AAC格式
|
||||||
|
*
|
||||||
|
* @author gc.x
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class ConverterTranFactories extends Thread implements Converter {
|
||||||
|
public volatile boolean runing = true;
|
||||||
|
/**
|
||||||
|
* 读流器
|
||||||
|
*/
|
||||||
|
private FFmpegFrameGrabber grabber;
|
||||||
|
/**
|
||||||
|
* 转码器
|
||||||
|
*/
|
||||||
|
private FFmpegFrameRecorder recorder;
|
||||||
|
/**
|
||||||
|
* 转FLV格式的头信息<br/>
|
||||||
|
* 如果有第二个客户端播放首先要返回头信息
|
||||||
|
*/
|
||||||
|
private byte[] headers;
|
||||||
|
/**
|
||||||
|
* 保存转换好的流
|
||||||
|
*/
|
||||||
|
private ByteArrayOutputStream stream;
|
||||||
|
/**
|
||||||
|
* 流地址,h264,aac
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
/**
|
||||||
|
* 流输出
|
||||||
|
*/
|
||||||
|
private List<AsyncContext> outEntitys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key用于表示这个转换器
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换队列
|
||||||
|
*/
|
||||||
|
private Map<String, Converter> factories;
|
||||||
|
|
||||||
|
public ConverterTranFactories(String url, String key, Map<String, Converter> factories,
|
||||||
|
List<AsyncContext> outEntitys, FFmpegFrameGrabber grabber) {
|
||||||
|
this.url = url;
|
||||||
|
this.key = key;
|
||||||
|
this.factories = factories;
|
||||||
|
this.outEntitys = outEntitys;
|
||||||
|
this.grabber = grabber;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
log.info("this url:{} converterTranFactories start", url);
|
||||||
|
grabber.setFrameRate(25);
|
||||||
|
if (grabber.getImageWidth() > 1920) {
|
||||||
|
grabber.setImageWidth(1920);
|
||||||
|
}
|
||||||
|
if (grabber.getImageHeight() > 1080) {
|
||||||
|
grabber.setImageHeight(1080);
|
||||||
|
}
|
||||||
|
stream = new ByteArrayOutputStream();
|
||||||
|
recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(),
|
||||||
|
grabber.getAudioChannels());
|
||||||
|
recorder.setInterleaved(true);
|
||||||
|
recorder.setVideoOption("preset", "ultrafast");
|
||||||
|
recorder.setVideoOption("tune", "zerolatency");
|
||||||
|
recorder.setVideoOption("crf", "25");
|
||||||
|
recorder.setGopSize(50);
|
||||||
|
recorder.setFrameRate(25);
|
||||||
|
recorder.setSampleRate(grabber.getSampleRate());
|
||||||
|
if (grabber.getAudioChannels() > 0) {
|
||||||
|
recorder.setAudioChannels(grabber.getAudioChannels());
|
||||||
|
recorder.setAudioBitrate(grabber.getAudioBitrate());
|
||||||
|
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
||||||
|
}
|
||||||
|
recorder.setFormat("flv");
|
||||||
|
recorder.setVideoBitrate(grabber.getVideoBitrate());
|
||||||
|
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||||
|
recorder.start();
|
||||||
|
if (headers == null) {
|
||||||
|
headers = stream.toByteArray();
|
||||||
|
stream.reset();
|
||||||
|
writeResponse(headers);
|
||||||
|
}
|
||||||
|
int nullNumber = 0;
|
||||||
|
while (runing) {
|
||||||
|
// 抓取一帧
|
||||||
|
Frame f = grabber.grab();
|
||||||
|
if (f != null) {
|
||||||
|
try {
|
||||||
|
// 转码
|
||||||
|
recorder.record(f);
|
||||||
|
} catch (Exception e) {
|
||||||
|
}
|
||||||
|
if (stream.size() > 0) {
|
||||||
|
byte[] b = stream.toByteArray();
|
||||||
|
stream.reset();
|
||||||
|
writeResponse(b);
|
||||||
|
if (outEntitys.isEmpty()) {
|
||||||
|
log.info("没有输出退出");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nullNumber++;
|
||||||
|
if (nullNumber > 200) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.sleep(5);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
closeConverter();
|
||||||
|
completeResponse();
|
||||||
|
log.info("this url:{} converterTranFactories exit", url);
|
||||||
|
factories.remove(this.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出FLV视频流
|
||||||
|
*
|
||||||
|
* @param b
|
||||||
|
*/
|
||||||
|
public void writeResponse(byte[] b) {
|
||||||
|
Iterator<AsyncContext> it = outEntitys.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
AsyncContext o = it.next();
|
||||||
|
try {
|
||||||
|
o.getResponse().getOutputStream().write(b);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.info("移除一个输出");
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出转换
|
||||||
|
*/
|
||||||
|
public void closeConverter() {
|
||||||
|
IOUtils.close(grabber);
|
||||||
|
IOUtils.close(recorder);
|
||||||
|
IOUtils.close(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭异步响应
|
||||||
|
*/
|
||||||
|
public void completeResponse() {
|
||||||
|
Iterator<AsyncContext> it = outEntitys.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
AsyncContext o = it.next();
|
||||||
|
o.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKey() {
|
||||||
|
return this.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUrl() {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addOutputStreamEntity(String key, AsyncContext entity) throws IOException {
|
||||||
|
if (headers == null) {
|
||||||
|
outEntitys.add(entity);
|
||||||
|
} else {
|
||||||
|
entity.getResponse().getOutputStream().write(headers);
|
||||||
|
entity.getResponse().getOutputStream().flush();
|
||||||
|
outEntitys.add(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exit() {
|
||||||
|
this.runing = false;
|
||||||
|
try {
|
||||||
|
this.join();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.gc.easy.flv.factories.state;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换器状态(初始化、打开、关闭、错误、运行)
|
||||||
|
*
|
||||||
|
* @author gc.x
|
||||||
|
*/
|
||||||
|
public enum ConverterState {
|
||||||
|
INITIAL, OPEN, CLOSE, ERROR, RUN
|
||||||
|
}
|
16
src/main/java/com/gc/easy/flv/service/IFLVService.java
Normal file
16
src/main/java/com/gc/easy/flv/service/IFLVService.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package com.gc.easy.flv.service;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public interface IFLVService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开一个流地址
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* @param response
|
||||||
|
*/
|
||||||
|
public void open(String url, HttpServletResponse response, HttpServletRequest request);
|
||||||
|
|
||||||
|
}
|
12
src/main/java/com/gc/easy/flv/service/IOpenFLVService.java
Normal file
12
src/main/java/com/gc/easy/flv/service/IOpenFLVService.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package com.gc.easy.flv.service;
|
||||||
|
|
||||||
|
public interface IOpenFLVService {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过通道号获取url
|
||||||
|
* @param channel
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getUrl(Integer channel);
|
||||||
|
}
|
83
src/main/java/com/gc/easy/flv/service/impl/FLVService.java
Normal file
83
src/main/java/com/gc/easy/flv/service/impl/FLVService.java
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package com.gc.easy.flv.service.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import com.gc.easy.flv.factories.Converter;
|
||||||
|
import com.gc.easy.flv.factories.ConverterFactories;
|
||||||
|
import com.gc.easy.flv.service.IFLVService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.servlet.AsyncContext;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FLV流转换
|
||||||
|
*
|
||||||
|
* @author gc.x
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class FLVService implements IFLVService {
|
||||||
|
|
||||||
|
private ConcurrentHashMap<String, Converter> converters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open(String url, HttpServletResponse response, HttpServletRequest request) {
|
||||||
|
String key = md5(url);
|
||||||
|
AsyncContext async = request.startAsync();
|
||||||
|
async.setTimeout(0);
|
||||||
|
if (converters.containsKey(key)) {
|
||||||
|
Converter c = converters.get(key);
|
||||||
|
try {
|
||||||
|
c.addOutputStreamEntity(key, async);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
throw new IllegalArgumentException(e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
List<AsyncContext> outs = new ArrayList<>();
|
||||||
|
outs.add(async);
|
||||||
|
ConverterFactories c = new ConverterFactories(url, key, converters, outs);
|
||||||
|
c.start();
|
||||||
|
converters.put(key, c);
|
||||||
|
}
|
||||||
|
response.setContentType("video/x-flv");
|
||||||
|
response.setHeader("Connection", "keep-alive");
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
try {
|
||||||
|
response.flushBuffer();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String md5(String plainText) {
|
||||||
|
StringBuilder buf = null;
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
md.update(plainText.getBytes());
|
||||||
|
byte b[] = md.digest();
|
||||||
|
int i;
|
||||||
|
buf = new StringBuilder("");
|
||||||
|
for (int offset = 0; offset < b.length; offset++) {
|
||||||
|
i = b[offset];
|
||||||
|
if (i < 0)
|
||||||
|
i += 256;
|
||||||
|
if (i < 16)
|
||||||
|
buf.append("0");
|
||||||
|
buf.append(Integer.toHexString(i));
|
||||||
|
}
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/main/resources/templates/video.html
Normal file
37
src/main/resources/templates/video.html
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>FLV Video Player</title>
|
||||||
|
|
||||||
|
<!-- 引入flv.js库 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<video id="videoPlayer" th:style="'width:' + ${width} + 'px; height:' + ${height} + 'px;'" controls></video>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
(function() {
|
||||||
|
var videoPath = /*[[${videoPath}]]*/ '';
|
||||||
|
|
||||||
|
if (flvjs.isSupported()) {
|
||||||
|
var videoElement = document.getElementById('videoPlayer');
|
||||||
|
var flvPlayer = flvjs.createPlayer({
|
||||||
|
type: 'flv',
|
||||||
|
url: videoPath
|
||||||
|
});
|
||||||
|
|
||||||
|
flvPlayer.attachMediaElement(videoElement);
|
||||||
|
flvPlayer.load();
|
||||||
|
|
||||||
|
// 播放视频
|
||||||
|
videoElement.play();
|
||||||
|
} else {
|
||||||
|
console.error('FLV not supported');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user