zoukankan      html  css  js  c++  java
  • 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("
    ");
    
            //人为在每个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("
    ");
                    }
                    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>
  • 相关阅读:
    C++实现网络寻路
    java实现生日相同概率
    java实现生日相同概率
    Mysql 锁表 for update (引擎/事务)
    mysql(for update)悲观锁总结与实践
    Select For update语句浅析
    Mysql查询语句使用select.. for update导致的数据库死锁分析
    数据库中Select For update语句的解析
    【转载】支付宝运营架构中柔性事务指的是什么?
    互联网支付系统整体架构详解
  • 原文地址:https://www.cnblogs.com/passedbylove/p/11841250.html
Copyright © 2011-2022 走看看