java使用ffmpeg生成HLS切片文件

   /***
     * 将文件切割成片
     * @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);
    }

猜你喜欢

转载自www.cnblogs.com/passedbylove/p/11841250.html
今日推荐