Use FFmpeg to list HLS live broadcast Convert m3u8 format to mp4 and save

Use FFmpeg to list HLS live broadcast Convert m3u8 format to mp4 and save

Saving live streaming as mp4 is a small function that needs to be completed recently.

We know that javacv is an efficient dependency package for processing audio and video in java. However, in the process of using it, the sweeper found that it did not support it The m3u8 format is processed as a video source, that is, the ffmpeg frame grabber collector cannot collect M3u8 format video (maybe the sweeper is not deep enough, and grabber has not been used to directly collect. M3u8 format video source).

javacv is still used in this process, but it is not used directly, but uses native FFmpeg (also in the dependent package, which can not be installed). If you need to install, please refer to: FFMpeg download and simple use

Before you start, learn about m3u8 file formats: Reading Note -m3u8 file format

Start:

1 Program project construction

1.1 build a springboot project,

Select a web project:

2 import dependency

<!--      javacv and ffmpeg Dependent packages for      -->
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.5.4</version>
    <exclusions>
        <exclusion>
            <groupId>org.bytedeco</groupId>
            <artifactId>*</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>ffmpeg-platform</artifactId>
    <version>4.3.1-1.5.4</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
    <scope>compile</scope>
</dependency>

3 code implementation

Project architecture

3.1 Hls to MP4

Since it is impossible to directly operate m3u8 files by collecting and recording, a more native method is used:

package com.saodisheng.processor;

import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacpp.Loader;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * description:
 * Convert the m3u8 playlist of hls to mp4 format according to the live address of hls
 * If the original video is not in m3u8 format, no intermediate conversion is required.
 *
 * javacv The collector FFmpegFrameGrabber does not support direct reading of m3u8 format files of hls,
 * Therefore, it is impossible to convert m3u8 to mp4 directly by collecting and recording.
 * The implementation process here is to directly operate ffmpeg,
 *
 * todo:Using ffmepg for format conversion is slow
 *
 * @author liuxingwu
 * @date 2022/1/9
 */
@Slf4j
public class HlsToMp4Processor {
    static final String DEST_VIDEO_TYPE = ".mp4";
    static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmssSSS");
    static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
    /**
     * Method entry
     *
     * @param sourceVideoPath   Video source path
     * @return
     */
    public static String process(String sourceVideoPath) {
        log.info("Start format conversion");
        if (!checkContentType(sourceVideoPath)) {
            log.info("Please enter.m3u8 Format file");
            return "";
        }
        // Get file name
        String destVideoPath = getFileName(sourceVideoPath)
                + "_" + SDF.format(new Date()) + DEST_VIDEO_TYPE;
        // Execute conversion logic
        return processToMp4(sourceVideoPath, destVideoPath) ? destVideoPath : "";
    }

    private static String getFileName(String sourceVideoPath) {
        return sourceVideoPath.substring(sourceVideoPath.contains("/") ?
                        sourceVideoPath.lastIndexOf("/") + 1 : sourceVideoPath.lastIndexOf("\\") + 1,
                sourceVideoPath.lastIndexOf("."));
    }

    /**
     * Execute conversion logic
     * @author saodisheng_liuxingwu
     * @modifyDate 2022/1/9
     */
    private static boolean processToMp4(String sourceVideoPath, String destVideoPath) {
        long startTime = System.currentTimeMillis();

        List<String> command = new ArrayList<String>();
        //Get the call path of ffmpeg local library in JavaCV
        String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
        command.add(ffmpeg);
        // Set supported network protocols
        command.add("-protocol_whitelist");
        command.add("concat,file,http,https,tcp,tls,crypto");
        command.add("-i");
        command.add(sourceVideoPath);
        command.add(destVideoPath);
        try {
            Process videoProcess = new ProcessBuilder(command).redirectErrorStream(true).start();

            fixedThreadPool.execute(new ReadStreamInfo(videoProcess.getErrorStream()));
            fixedThreadPool.execute(new ReadStreamInfo(videoProcess.getInputStream()));

            videoProcess.waitFor();

            log.info("Intermediate conversion completed, generated file:" + destVideoPath);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("Time use:" + (int)((endTime - startTime) / 1000) + "second");
        }
    }

    /**
     * Verify whether it is a m3u8 file
     * @author saodisheng_liuxingwu
     * @modifyDate 2022/1/9
    */
    private static boolean checkContentType(String filePath) {
        if (StringUtils.isEmpty(filePath)) {
            return false;
        }
        String type = filePath.substring(filePath.lastIndexOf(".") + 1, filePath.length()).toLowerCase();
        return "m3u8".equals(type);
    }
}
package com.saodisheng.processor;

import java.io.InputStream;

/**
 * description:
 * Running time getRuntime(). Exec () or processbuilder (array) After start() creates the child Process,
 * The output information and error information of the sub process must be taken away in time, otherwise the output information flow and error information flow may be filled up due to too much information,
 * Eventually, the child process is blocked and cannot be executed.
 *
 * @author liuxingwu_saodisheng(01420175)
 * @date 2022/1/10
 */
public class ReadStreamInfo extends Thread {
    InputStream is = null;
    public ReadStreamInfo(InputStream is) {
        this.is = is;
    }

    @Override
    public void run() {
        try {
            while(true) {
                int ch = is.read();
                if(ch != -1) {
                    System.out.print((char)ch);
                } else {
                    break;
                }
            }
            if (is != null) {
                is.close();
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.2 realization of ejector

In fact, if only the converted MP4 file is retained locally, no additional processing is required. However, if you want to push it to the server or directly push it to the specified file, you need to use the following streamer.

Of course, if the processed original video is not a m3u8 format file, the basic video format conversion can also be realized by directly using the streaming device.

package com.saodisheng.processor;

import org.apache.commons.lang3.StringUtils;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;

import java.io.InputStream;
import java.io.OutputStream;

/**
 * description:
 * Video streaming device (push MP4 here)
 * If the original video is not in the m3u8 format of hls, you can directly call the streaming device for format conversion and push
 *
 * @author liuxingwu_saodisheng(01420175)
 * @date 2022/1/13
 */
public class VideoPusher {
    /** Collector**/
    private FFmpegFrameGrabber grabber;
    /** Recorder**/
    private FFmpegFrameRecorder recorder;

    static final String DEST_VIDEO_TYPE = ".mp4";

    public VideoPusher() {
        // In ffmpegframegrabber Set FFmpeg log level before start()
        avutil.av_log_set_level(avutil.AV_LOG_INFO);
        FFmpegLogCallback.set();
    }

    /**
     * Processing video sources
     * One of the input stream and output address must be a valid input
     * @param inputStream   Input stream
     * @param inputAddress  Enter address
     * @return
     */
    public VideoPusher from(InputStream inputStream, String inputAddress) {
        if (inputStream != null) {
            grabber = new FFmpegFrameGrabber(inputStream);
        } else if (StringUtils.isNotEmpty(inputAddress)) {
            grabber = new FFmpegFrameGrabber(inputAddress);
        } else {
            throw new RuntimeException("Video source is null error, please confirm to enter a valid video source");
        }

        // Start collection
        try {
            grabber.start();
        } catch (FrameGrabber.Exception e) {
            e.printStackTrace();
        }

        return this;
    }

    public VideoPusher from(InputStream inputStream) {
        return from(inputStream, null);
    }

    public VideoPusher from(String inputAddress) {
        return from(null, inputAddress);
    }

    /**
     * Set output
     *
     * @param outputStream
     * @param outputAddress
     * @return
     */
    public VideoPusher to(OutputStream outputStream, String outputAddress) {
        if (outputStream != null) {
            recorder = new FFmpegFrameRecorder(outputStream, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
        } else if (StringUtils.isNotEmpty(outputAddress)) {
            recorder = new FFmpegFrameRecorder(outputAddress, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
        } else {
            throw new RuntimeException("The input path is null error. Please specify the correct input path or output stream");
        }
        // Format
        recorder.setFormat(DEST_VIDEO_TYPE);

        recorder.setOption("method", "POST");
        recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
        recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);

        // start recording 
        try {
            recorder.start(grabber.getFormatContext());
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
        }
        return this;
    }

    public VideoPusher to(OutputStream outputStream) {
        return to(outputStream, null);
    }

    public VideoPusher to(String outputAddress) {
        return to(null, outputAddress);
    }


    /**
     * Push stream
     */
    public void go() {
        AVPacket pkt;
        try {
            while ((pkt = grabber.grabPacket()) != null) {
                recorder.recordPacket(pkt);
            }
        } catch (FrameGrabber.Exception | FrameRecorder.Exception e) {
            e.printStackTrace();
        }
        close();
    }

    public void close() {
        try {
            if (recorder != null) {
                recorder.close();
            }
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
        }
        try {
            if (grabber != null) {
                // Because grabber's close calls stop and release, and stop also calls release, release is directly used to prevent repeated calls
                grabber.release();
            }
        } catch (FrameGrabber.Exception e) {
            e.printStackTrace();
        }
    }
}

3.3 parameters of analog receiving front end

package com.saodisheng.controller;

import com.saodisheng.controller.vo.VideoParamsVO;

import com.saodisheng.processor.HlsToMp4Processor;
import com.saodisheng.processor.VideoPusher;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;


/**
 * description:
 *
 * @author liuxingwu_saodisheng(01420175)
 * @date 2022/1/12
 */
@RestController
@Slf4j
public class HlsToMp4TestController {

    @PostMapping("convert_to_mp4/")
    public void convertToMp4(@RequestBody VideoParamsVO dataVO) {
        String sourceVideoUrl = dataVO.getSourceVideoUrl();
        Assert.notNull(sourceVideoUrl, "Video source cannot be empty");

        // Convert m3u8 format video to mp4 local file (intermediate file for format conversion)
        String destFileName = HlsToMp4Processor.process(sourceVideoUrl);
        if (StringUtils.isEmpty(destFileName)) {
            log.error("operation failed");
        }

        // Push flow
        if(StringUtils.isNotEmpty(dataVO.getDestVideoPath())) {
            new VideoPusher().from(destFileName).to(dataVO.getDestVideoPath() + destFileName).go();
            // Delete intermediate file
            new File(destFileName).delete();
        }
    }
}
package com.saodisheng.controller.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * description: Receive front-end data
 *
 * @author liuxingwu_saodisheng(01420175)
 * @date 2022/1/13
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class VideoParamsVO implements Serializable {
    /** Video source address m3u8 format**/
    private String sourceVideoUrl;

    /** Push destination address**/
    private String destVideoPath;
}

4 start program test

After starting the program, use the API post interface debugging tool to debug:

5 precautions

Because the ffmpeg here depends on the package, not the ffmpeg downloaded locally Exe plug-in, so the absolute path should be used for the ts path in the local m3u8 format file. If ffmpeg is used Exe plug-in, ffmpeg is configured in the system environment variable_ After home, change the above code to:

/**
     * Execute conversion logic
     * @author saodisheng_liuxingwu
     * @modifyDate 2022/1/9
     */
    private static boolean processToMp4(String sourceVideoPath, String destVideoPath) {
        long startTime = System.currentTimeMillis();

        List<String> command = new ArrayList<String>();
//        //Get the call path of ffmpeg local library in JavaCV
//        String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
        command.add("ffmpeg");
        // Set supported network protocols
        command.add("-protocol_whitelist");
        command.add("concat,file,http,https,tcp,tls,crypto");
        command.add("-i");
        command.add(sourceVideoPath);
        command.add(destVideoPath);
        try {
            Process videoProcess = new ProcessBuilder(command).redirectErrorStream(true).start();

            fixedThreadPool.execute(new ReadStreamInfo(videoProcess.getErrorStream()));
            fixedThreadPool.execute(new ReadStreamInfo(videoProcess.getInputStream()));

            videoProcess.waitFor();

            log.info("Intermediate conversion completed, generated file:" + destVideoPath);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("Time use:" + (int)((endTime - startTime) / 1000) + "second");
        }
    }

Then, for the playlist in m3u8, there is no need to point to the absolute path.

Code link

Keywords: ffmpeg

Added by Alien on Sat, 15 Jan 2022 09:31:25 +0200