/***
* 将文件切割成片
* @param filename
* @param uuid
* @param data
* @throws IOException
*/
default void divideToSegments(String filename, String uuid, byte[]data) throws IOException {
DivideTask divideTask = new DivideTask(filename,uuid,data);
Future<ImmutablePair<PlayList, List<TransportSegment>>> divideFuture = getThreadPool().submit(divideTask);
String mediaId = String.format("media.%s",uuid);
try {
ImmutablePair<PlayList, List<TransportSegment>> plsAndTsFiles = divideFuture.get(30, TimeUnit.MINUTES);
PlayList playlist = plsAndTsFiles.getLeft();
List<TransportSegment> segments = plsAndTsFiles.getRight();
//保存切片文件
saveSegments(segments);
//保存播放列表
savePlayList(playlist);
//放到缓存里
Map<String,String> mapping = new HashMap<>();
mapping.put("playlist",playlist.getContext());
//把原始文件放进去,方便以后下载
mapping.put("binary",Base64.getEncoder().encodeToString(Files.readAllBytes(Paths.get(filename))));
for (TransportSegment segment:segments)
{
String tsFileName = segment.getFilename();
byte[] bytes = segment.getBytes();
String binary = Base64.getEncoder().encodeToString(bytes);
mapping.put(tsFileName,binary);
}
//切片以后的文件添加到缓存
getCacheService().setCacheMap(mediaId, mapping);
//30分钟以后失效
getCacheService().expire(mediaId,7,TimeUnit.DAYS);
} catch (InterruptedException| ExecutionException | TimeoutException e) {
getLogger().error("文件切片失败:{}",e);
}
}
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Table;
import java.time.LocalDateTime;
import java.time.ZoneId;
/***
* 转换后的文件切片
*/
@Data
@NoArgsConstructor
@Table(name = "open_segment")
public class TransportSegment
{
String uuid;
/***
* 文件名
*/
private String filename;
/***
* 字节流
*/
private byte[] bytes;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8"));
private TransportSegment(Builder builder) {
setUuid(builder.uuid);
setFilename(builder.filename);
setBytes(builder.bytes);
setCreateTime(builder.createTime);
}
public static final class Builder {
private String uuid;
private String filename;
private byte[] bytes;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8"));
public Builder() {
}
public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
}
public Builder filename(String filename) {
this.filename = filename;
return this;
}
public Builder bytes(byte[] bytes) {
this.bytes = bytes;
return this;
}
public Builder createTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
}
public TransportSegment build() {
return new TransportSegment(this);
}
}
}
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
@NoArgsConstructor
@Data
public class PlayList {
private String uuid;
/***
* 播放时长
*/
private Float duration;
/***
* 播放列表内容
*/
private String context;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8"));
private PlayList(Builder builder) {
setUuid(builder.uuid);
setDuration(builder.duration);
setContext(builder.context);
setCreateTime(builder.createTime);
}
public static final class Builder {
private String uuid;
private Float duration;
private String context;
private LocalDateTime createTime = LocalDateTime.now(ZoneId.of("+8"));
public Builder() {
}
public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
}
public Builder duration(Float duration) {
this.duration = duration;
return this;
}
public Builder context(String context) {
this.context = context;
return this;
}
public Builder createTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
}
public PlayList build() {
return new PlayList(this);
}
}
}
/**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
@Override
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) {
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<String, T> entry : dataMap.entrySet()) {
String hashKey = entry.getKey();
if(hashKey !=null){
hashOperations.put(key, hashKey, entry.getValue());
}
else {
log.error("出错了:{},hash键为null@{}",entry.getValue());
}
}
}
return hashOperations;
}
@Override
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
import lombok.Data;
import lombok.NoArgsConstructor;
import net.bramp.ffmpeg.probe.FFmpegStream;
import org.apache.ibatis.type.JdbcType;
import tk.mybatis.mapper.annotation.ColumnType;
import javax.persistence.Table;
import java.util.List;
@Data
@NoArgsConstructor
@Table(name = "open_media")
public class AudioMediaFile extends MediaFile {
/***
* 流通道数
*/
@ColumnType(column = "nb_streams",jdbcType = JdbcType.TINYINT)
byte nbStreams;
byte nbPrograms;
Integer startTime;
/***
* 格式名称
*/
String formatName;
/***
* 多媒体播放时长
*/
Float duration;
/***
* 比特率
*/
Integer bitRate;
@ColumnType(column = "probe_score",jdbcType = JdbcType.TINYINT)
byte probeScore;
/***
* 文件类型
*/
@ColumnType(column = "type",jdbcType = JdbcType.TINYINT)
byte type;
List<FFmpegStream> streams;
String metadata;
private AudioMediaFile(Builder builder) {
setUuid(builder.uuid);
setName(builder.name);
setData(builder.data);
setMimeType(builder.mimeType);
setStamp(builder.stamp);
setSize(builder.size);
setNbStreams(builder.nbStreams);
setFormatName(builder.formatName);
setDuration(builder.duration);
setBitRate(builder.bitRate);
setProbeScore(builder.probeScore);
setType(builder.type);
setStreams(builder.streams);
setMetadata(builder.metadata);
}
public static final class Builder {
private String uuid;
private String name;
private byte[] data;
private String mimeType;
private Long stamp;
private Long size;
private byte nbStreams;
private String formatName;
private Float duration;
private Integer bitRate;
private byte probeScore;
private byte type;
private List<FFmpegStream> streams;
private String metadata;
public Builder() {
}
public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder data(byte[] data) {
this.data = data;
return this;
}
public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
public Builder stamp(Long stamp) {
this.stamp = stamp;
return this;
}
public Builder size(Long size) {
this.size = size;
return this;
}
public Builder nbStreams(byte nbStreams) {
this.nbStreams = nbStreams;
return this;
}
public Builder formatName(String formatName) {
this.formatName = formatName;
return this;
}
public Builder duration(Float duration) {
this.duration = duration;
return this;
}
public Builder bitRate(Integer bitRate) {
this.bitRate = bitRate;
return this;
}
public Builder probeScore(byte probeScore) {
this.probeScore = probeScore;
return this;
}
public Builder type(byte type) {
this.type = type;
return this;
}
public Builder streams(List<FFmpegStream> streams) {
this.streams = streams;
return this;
}
public Builder metadata(String metadata) {
this.metadata = metadata;
return this;
}
public AudioMediaFile build() {
return new AudioMediaFile(this);
}
}
}
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/***
* 多媒体文件
*/
@Data
@NoArgsConstructor
@Table(name = "open_media")
public class MediaFile {
/**
* 音频文件
*/
public static final byte TYPE_AUDIO = 0x1;
public static final byte TYPE_VIDEO = 0x2;
public static final byte TYPE_DATA1 = 0x4;
public static final byte TYPE_DATA2 = 0x8;
/***
* 文件唯一标识
*/
@Id
@GeneratedValue(generator = "JDBC")
String uuid;
/****
* 文件名
*/
String name;
/***
* 解析后的数据流
*/
byte[] data;
/***
* 多媒体文件类型
*/
String mimeType;
/***
* 创建文件的时间
*/
Long stamp;
/***
* 文件大小
*/
Long size;
private MediaFile(Builder builder) {
setUuid(builder.uuid);
setName(builder.name);
setData(builder.data);
setMimeType(builder.mimeType);
setStamp(builder.stamp);
setSize(builder.size);
}
public static final class Builder {
private String uuid;
private String name;
private byte[] data;
private String mimeType;
private Long stamp;
private Long size;
public Builder() {
}
public Builder uuid(String uuid) {
this.uuid = uuid;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder data(byte[] data) {
this.data = data;
return this;
}
public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}
public Builder stamp(Long stamp) {
this.stamp = stamp;
return this;
}
public Builder size(Long size) {
this.size = size;
return this;
}
public MediaFile build() {
return new MediaFile(this);
}
}
}
static String toJson(Object value) throws JsonProcessingException {
ObjectMapper objectMapper =new ObjectMapper();
//属性值为null不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//默认值的不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
//反斜杠转义其他字符
objectMapper.configure(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER,true);
//所有键值用字符串形式包装起来
objectMapper.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS,true);
return objectMapper.writeValueAsString(value);
}
import xxx.bean.AudioMediaFile;
import xxx.bean.MediaFile;
import xxx.bean.PlayList;
import xxx.bean.TransportSegment;
import xxx.service.MediaService;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFmpegUtils;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.job.FFmpegJob;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.probe.FFmpegStream;
import net.bramp.ffmpeg.progress.Progress;
import net.bramp.ffmpeg.progress.ProgressListener;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/***
* 文件切割线程任务
* divides it into a series of small media segments of equal duration.
* @author dqk
*/
@Deprecated
@Slf4j
public class DivideTask implements Callable<ImmutablePair<PlayList, List<TransportSegment>>>
{
final Locale locale = Locale.US;
final FFmpeg ffmpeg = new FFmpeg();
final FFprobe ffprobe = new FFprobe();
ImmutablePair<FFmpegProbeResult, AudioMediaFile> pair;
String filename;
String uuid;
byte[] data;
public DivideTask(ImmutablePair<FFmpegProbeResult,AudioMediaFile> pair) throws IOException {
this.pair = pair;
}
public DivideTask(String filename,String uuid,byte[] data) throws IOException {
this.filename = filename;
this.uuid = uuid;
this.data = data;
//获取反序列化后文件的元数据信息
FFmpegProbeResult probeResult = ffprobe.probe(filename);
long timestamp = LocalDateTime.now(ZoneId.of("UTC+8")).toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
String metadata = MediaService.toJson(probeResult);
AudioMediaFile.Builder builder = new AudioMediaFile.Builder()
.name(filename)
.uuid(uuid)
.streams(probeResult.streams)
.mimeType(probeResult.format.format_long_name)
.type(MediaFile.TYPE_AUDIO)
.stamp(timestamp)
.bitRate(Long.valueOf(probeResult.format.bit_rate).intValue())
.duration(Double.valueOf(probeResult.format.duration).floatValue())
.formatName(probeResult.format.format_name)
.nbStreams((byte) probeResult.format.nb_streams)
.size(probeResult.format.size)
.probeScore((byte) probeResult.format.probe_score)
.mimeType(probeResult.format.format_long_name)
.data(data)
.metadata(metadata);
this.pair = new ImmutablePair<>(probeResult,builder.build());
}
public static String getString(InputStream stream) throws IOException {
return IOUtils.toString(stream,"UTF-8");
}
@Override
public ImmutablePair<PlayList,List<TransportSegment>> call() throws Exception {
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
final FFmpegProbeResult probe = pair.getLeft();
AudioMediaFile audioFile = pair.getRight();
final List<FFmpegStream> streams = probe.getStreams().stream().filter(fFmpegStream -> fFmpegStream.codec_type!=null).collect(Collectors.toList());
final Optional<FFmpegStream> audioStream = streams.stream().filter(fFmpegStream -> FFmpegStream.CodecType.AUDIO.equals(fFmpegStream.codec_type)).findFirst();
if(!audioStream.isPresent())
{
log.error("未发现音频流");
}
String filename = probe.format.filename;
Path nioFile = Paths.get(filename);
String directory = nioFile.getParent().toString();
String uuid = audioFile.getUuid();
String output = String.format("%s%sstream.m3u8",directory, File.separator);
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(filename)
.overrideOutputFiles(true)
.addOutput(output)
.setFormat("wav")
.setAudioBitRate(audioStream.isPresent()?audioStream.get().bit_rate:0)
.setAudioChannels(1)
.setAudioCodec("aac") // using the aac codec
.setAudioSampleRate(audioStream.get().sample_rate)
.setAudioBitRate(audioStream.get().bit_rate)
.setStrict(FFmpegBuilder.Strict.STRICT)
.setFormat("hls")
.addExtraArgs("-hls_wrap", "0", "-hls_time", "5", "-hls_list_size","0")
.done();
FFmpegJob job =
executor.createJob(
builder,
new ProgressListener() {
// Using the FFmpegProbeResult determine the duration of the input
final double duration_ns = probe.getFormat().duration * TimeUnit.SECONDS.toNanos(1);
@Override
public void progress(Progress progress) {
double percentage = progress.out_time_ns / duration_ns;
// Print out interesting information about the progress
String consoleLog = String.format(
locale,
"[%.0f%%] status:%s frame:%d time:%s fps:%.0f speed:%.2fx",
percentage * 100,
progress.status,
progress.frame,
FFmpegUtils.toTimecode(progress.out_time_ns, TimeUnit.NANOSECONDS),
progress.fps.doubleValue(),
progress.speed);
log.debug(consoleLog);
}
});
job.run();
if (job.getState() == FFmpegJob.State.FINISHED) {
//排除的文件
String[] excludes = new String[]{
"wav","m3u8"
};
List<TransportSegment> segments = Files.list(Paths.get(directory)).filter(
path -> {
String extension = getFileExtension(path.getFileName().toString());
return !Arrays.asList(excludes).contains(extension);
}
).map(path -> {
String name = path.getFileName().toString();
try {
byte[] bytes = IOUtils.toByteArray(path.toUri());
TransportSegment segment = new TransportSegment
.Builder()
.bytes(bytes)
.filename(name)
.uuid(uuid)
.build();
return segment;
} catch (IOException e) {
log.error("读取文件失败:{}",e);
}
return null;
}).collect(Collectors.toList());
String context = getString(new FileInputStream(output));
PlayList playList = new PlayList.Builder()
.context(context)
.uuid(uuid)
.duration(Double.valueOf(probe.format.duration).floatValue())
.build();
return new ImmutablePair<>(playList,segments);
}else {
log.error("文件分割发生不可预料的错误:{}");
}
return null;
}
private static String getFileExtension(String fileName) {
if (fileName.lastIndexOf(".") != -1 && fileName.lastIndexOf(".") != 0) {
return fileName.substring(fileName.lastIndexOf(".") + 1);
} else {
return "";
}
}
}
最终生成结果
前端代码:
var ctlVolume =$("#volume");
//音量
var level = ctlVolume.attr("min")/ctlVolume.attr("max");
var player = videojs('example-video');
// player.ready(function() {
// var _this = this
// //速率
// var playbackRate = $("#playbackRate").val();
// var speed = parseFloat(playbackRate);
//
// var volume = parseFloat($("#volume").val()/100.0);
//
// setTimeout(function() {
// _this.playbackRate(speed);
// _this.volume(volume);
// },20);
// });
var data = response.data;
var message = '消息:'+response.message+",code:"+response.code+",meta:"+JSON.stringify(data);
console.info(message);
player.src('/media/'+data.uuid+'.m3u8');
player.play();
@RequestMapping(value = "{uuid}.m3u8")
public ResponseEntity<StreamingResponseBody> m3u8Generator(@PathVariable("uuid") String uuid){
String key = "media.".concat(uuid);
Map<String, Object> cached = cacheService.getCacheMap(key);
if(CollectionUtils.isEmpty(cached))
{
return new ResponseEntity(null, HttpStatus.OK);
}
String playlist = (String) cached.get("playlist");
String[] lines = playlist.split("\n");
//人为在每个MPEG-2 transport stream文件前面加上一个地址前缀
StringBuffer buffer = new StringBuffer();
StreamingResponseBody responseBody = new StreamingResponseBody() {
@Override
public void writeTo (OutputStream out) throws IOException {
for(int i = 0; i < lines.length; i++)
{
String line = lines[i];
if(line.endsWith(".ts"))
{
buffer.append("/streaming/");
buffer.append(uuid);
buffer.append("/");
buffer.append(line);
}else {
buffer.append(line);
}
buffer.append("\r\n");
}
out.write(buffer.toString().getBytes());
out.flush();
}
};
return new ResponseEntity(responseBody, HttpStatus.OK);
}
2020-1-7日更新
方法补充:getCacheService().setCacheMap(mediaId, mapping);
/**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
<T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap);
/**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
@Override
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) {
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
hashOperations.putAll(key, dataMap);
}
return hashOperations;
}
saveSegments方法
<insert id="saveSegments">
INSERT INTO open_segment(
uuid
,filename
,bytes
,create_time
)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.uuid}
,#{item.filename}
,#{item.bytes}
,#{item.createTime}
)
</foreach>
</insert>
savePlayList方法
<insert id="savePlayList" useGeneratedKeys="true" keyProperty="id">
insert into open_playlist(uuid, duration, context, create_time)
values (#{uuid}, #{duration}, #{context}, #{createTime})
</insert>