FFmpeg+javacpp中仿ffplay播放

FFmpeg+javacpp+javacv使用
ffmpeg-6.0\fftools\ffplay.c

1、ffplay 基于 SDL 和 FFmpeg 库的简单媒体播放器

ffmpeg-6.0\fftools\ffplay.cFFplay是一款使用FFmpeg库SDL库的非常简单便携的媒体播放器。它主要用作各种FFmpeg API的测试平台。

parse_options(NULL, argc, argv, options, opt_input_file);
is = stream_open(input_filename, file_iformat);
event_loop(is);
实质刷新每一帧流refresh_loop_wait_event(cur_stream, &event);video_refresh获取,对应到 FFmpeg帧捕获器 : FFmpegFrameGrabber

ffplay -i “G:\视频\动漫\长安三万里2023.mp4” -hide_banner -x 800 -y 600

/* Called from the main */
int main(int argc, char **argv)
{
    int flags;
    VideoState *is;

    init_dynload();

    av_log_set_flags(AV_LOG_SKIP_REPEATED);
    parse_loglevel(argc, argv, options);

    /* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
    avdevice_register_all();
#endif
    avformat_network_init();

    signal(SIGINT , sigterm_handler); /* Interrupt (ANSI).    */
    signal(SIGTERM, sigterm_handler); /* Termination (ANSI).  */

    show_banner(argc, argv, options);

    parse_options(NULL, argc, argv, options, opt_input_file);

    if (!input_filename) {
        show_usage();
        av_log(NULL, AV_LOG_FATAL, "An input file must be specified\n");
        av_log(NULL, AV_LOG_FATAL,
               "Use -h to get full help or, even better, run 'man %s'\n", program_name);
        exit(1);
    }

    if (display_disable) {
        video_disable = 1;
    }
    flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
    if (audio_disable)
        flags &= ~SDL_INIT_AUDIO;
    else {
        /* Try to work around an occasional ALSA buffer underflow issue when the
         * period size is NPOT due to ALSA resampling by forcing the buffer size. */
        if (!SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"))
            SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE","1", 1);
    }
    if (display_disable)
        flags &= ~SDL_INIT_VIDEO;
    if (SDL_Init (flags)) {
        av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError());
        av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
        exit(1);
    }

    SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);
    SDL_EventState(SDL_USEREVENT, SDL_IGNORE);

    if (!display_disable) {
        int flags = SDL_WINDOW_HIDDEN;
        if (alwaysontop)
#if SDL_VERSION_ATLEAST(2,0,5)
            flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else
            av_log(NULL, AV_LOG_WARNING, "Your SDL version doesn't support SDL_WINDOW_ALWAYS_ON_TOP. Feature will be inactive.\n");
#endif
        if (borderless)
            flags |= SDL_WINDOW_BORDERLESS;
        else
            flags |= SDL_WINDOW_RESIZABLE;

#ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
        SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
#endif
        window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);
        SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
        if (window) {
            renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
            if (!renderer) {
                av_log(NULL, AV_LOG_WARNING, "Failed to initialize a hardware accelerated renderer: %s\n", SDL_GetError());
                renderer = SDL_CreateRenderer(window, -1, 0);
            }
            if (renderer) {
                if (!SDL_GetRendererInfo(renderer, &renderer_info))
                    av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n", renderer_info.name);
            }
        }
        if (!window || !renderer || !renderer_info.num_texture_formats) {
            av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s", SDL_GetError());
            do_exit(NULL);
        }
    }

    is = stream_open(input_filename, file_iformat);
    if (!is) {
        av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
        do_exit(NULL);
    }

    event_loop(is);

    /* never returns */

    return 0;
}
static void video_refresh(void *opaque, double *remaining_time)
{
    VideoState *is = opaque;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
        check_external_clock_speed(is);

    if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
            video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
    }

    if (is->video_st) {
retry:
        if (frame_queue_nb_remaining(&is->pictq) == 0) {
            // nothing to do, no picture to display in the queue
        } else {
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* dequeue the picture */
            lastvp = frame_queue_peek_last(&is->pictq);
            vp = frame_queue_peek(&is->pictq);

            if (vp->serial != is->videoq.serial) {
                frame_queue_next(&is->pictq);
                goto retry;
            }

            if (lastvp->serial != vp->serial)
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                goto display;

            /* compute nominal last_duration */
            last_duration = vp_duration(is, lastvp, vp);
            delay = compute_target_delay(last_duration, is);

            time= av_gettime_relative()/1000000.0;
            if (time < is->frame_timer + delay) {
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            is->frame_timer += delay;
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                is->frame_timer = time;

            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);
                if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
                    is->frame_drops_late++;
                    frame_queue_next(&is->pictq);
                    goto retry;
                }
            }

            if (is->subtitle_st) {
                while (frame_queue_nb_remaining(&is->subpq) > 0) {
                    sp = frame_queue_peek(&is->subpq);

                    if (frame_queue_nb_remaining(&is->subpq) > 1)
                        sp2 = frame_queue_peek_next(&is->subpq);
                    else
                        sp2 = NULL;

                    if (sp->serial != is->subtitleq.serial
                            || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                            || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                    {
                        if (sp->uploaded) {
                            int i;
                            for (i = 0; i < sp->sub.num_rects; i++) {
                                AVSubtitleRect *sub_rect = sp->sub.rects[i];
                                uint8_t *pixels;
                                int pitch, j;

                                if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {
                                    for (j = 0; j < sub_rect->h; j++, pixels += pitch)
                                        memset(pixels, 0, sub_rect->w << 2);
                                    SDL_UnlockTexture(is->sub_texture);
                                }
                            }
                        }
                        frame_queue_next(&is->subpq);
                    } else {
                        break;
                    }
                }
            }

            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            if (is->step && !is->paused)
                stream_toggle_pause(is);
        }
display:
        /* display picture */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display(is);
    }
    is->force_refresh = 0;
    if (show_status) {
        AVBPrint buf;
        static int64_t last_time;
        int64_t cur_time;
        int aqsize, vqsize, sqsize;
        double av_diff;

        cur_time = av_gettime_relative();
        if (!last_time || (cur_time - last_time) >= 30000) {
            aqsize = 0;
            vqsize = 0;
            sqsize = 0;
            if (is->audio_st)
                aqsize = is->audioq.size;
            if (is->video_st)
                vqsize = is->videoq.size;
            if (is->subtitle_st)
                sqsize = is->subtitleq.size;
            av_diff = 0;
            if (is->audio_st && is->video_st)
                av_diff = get_clock(&is->audclk) - get_clock(&is->vidclk);
            else if (is->video_st)
                av_diff = get_master_clock(is) - get_clock(&is->vidclk);
            else if (is->audio_st)
                av_diff = get_master_clock(is) - get_clock(&is->audclk);

            av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
            av_bprintf(&buf,
                      "%7.2f %s:%7.3f fd=%4d aq=%5dKB vq=%5dKB sq=%5dB f=%"PRId64"/%"PRId64"   \r",
                      get_master_clock(is),
                      (is->audio_st && is->video_st) ? "A-V" : (is->video_st ? "M-V" : (is->audio_st ? "M-A" : "   ")),
                      av_diff,
                      is->frame_drops_early + is->frame_drops_late,
                      aqsize / 1024,
                      vqsize / 1024,
                      sqsize,
                      is->video_st ? is->viddec.avctx->pts_correction_num_faulty_dts : 0,
                      is->video_st ? is->viddec.avctx->pts_correction_num_faulty_pts : 0);

            if (show_status == 1 && AV_LOG_INFO > av_log_get_level())
                fprintf(stderr, "%s", buf.str);
            else
                av_log(NULL, AV_LOG_INFO, "%s", buf.str);

            fflush(stderr);
            av_bprint_finalize(&buf, NULL);

            last_time = cur_time;
        }
    }
}

2、FFmpeg帧捕获器 : FFmpegFrameGrabber

JavaCV 1.5.12 API
JavaCPP Presets for FFmpeg 7.1.1-1.5.12 API

在这里插入图片描述

2.1 grabSamples()

grabber.grabSamples() 获取音频信息
javax.sound.sampled.AudioFormat 设置PCM编码的AudioFormat参数,注意每个样本中的位数,无法获取,一般默认设置16位深。

package org.xhbruce.test;

import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.xhbruce.Log.XLog;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;

public class AudioPlayer implements Runnable {
    private String filePath;

    public AudioPlayer(String filePath) {
        this.filePath = filePath;
    }

    public void playAudio() throws Exception {
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(filePath);
        grabber.start();

        int sampleRate = grabber.getSampleRate();
        int audioChannels = grabber.getAudioChannels();
        int audioBitsPerSample = getAudioBitsPerSample(grabber);
        XLog.d("audioBitsPerSample=" + audioBitsPerSample);
        if (audioBitsPerSample <= 0) {
            audioBitsPerSample = 16; // 默认使用16位深度
        }

        AudioFormat audioFormat = new AudioFormat(
                (float) sampleRate,
                audioBitsPerSample,
                audioChannels,
                true,
                false
        );

        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
        SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info);
        line.open(audioFormat);
        line.start();

        Frame frame;
        while ((frame = grabber.grabSamples()) != null) {
            if (frame.samples == null || frame.samples.length == 0) continue;

            Buffer buffer = frame.samples[0];
            byte[] audioBytes;

            if (buffer instanceof ShortBuffer) {
                ShortBuffer shortBuffer = (ShortBuffer) buffer;
                audioBytes = new byte[shortBuffer.remaining() * 2]; // Each short is 2 bytes
                for (int i = 0; i < shortBuffer.remaining(); i++) {
                    short value = shortBuffer.get(i);
                    audioBytes[i * 2] = (byte) (value & 0xFF);
                    audioBytes[i * 2 + 1] = (byte) ((value >> 8) & 0xFF);
                }
            } else if (buffer instanceof ByteBuffer) {
                ByteBuffer byteBuffer = (ByteBuffer) buffer;
                audioBytes = new byte[byteBuffer.remaining()];
                byteBuffer.get(audioBytes);
            } else {
                throw new IllegalArgumentException("Unsupported buffer type: " + buffer.getClass());
            }

            line.write(audioBytes, 0, audioBytes.length);
        }

        line.drain();
        line.stop();
        line.close();
        grabber.stop();
    }

    private int getAudioBitsPerSample(FFmpegFrameGrabber grabber) {
        AVFormatContext formatContext = grabber.getFormatContext();
        if (formatContext == null) {
            return -1;
        }

        int nbStreams = formatContext.nb_streams();
        for (int i = 0; i < nbStreams; i++) {
            AVStream stream = formatContext.streams(i);
            if (stream.codecpar().codec_type() == avutil.AVMEDIA_TYPE_AUDIO) {
                AVCodecParameters codecParams = stream.codecpar();
                return codecParams.bits_per_raw_sample();
            }
        }

        return -1;
    }

    @Override
    public void run() {
        try {
            playAudio();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
//        new Thread(new AudioPlayer("G:\\视频\\动漫\\长安三万里2023.mp4")).start();
        new Thread(new AudioPlayer("F:\\Music\\Let Me Down Slowly.mp3")).start();
    }
}

2.2 grabImage()

grabImage() 获取视频信息
JFrame、JLabel图片显示

package org.xhbruce.test;

import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;

import javax.swing.*;
import java.awt.*;

public class VideoPlayer extends JFrame implements Runnable {
    private String filePath;
    private JLabel videoLabel;

    public VideoPlayer(String title, String filePath) {
        super(title);
        this.filePath = filePath;
        setSize(800, 600);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);

        videoLabel = new JLabel();
        getContentPane().add(videoLabel, BorderLayout.CENTER);
    }

    public void playVideo() throws Exception {
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(filePath);
        grabber.start();

        int width = grabber.getImageWidth();
        int height = grabber.getImageHeight();
        Java2DFrameConverter converter = new Java2DFrameConverter();

        Frame frame;
        while ((frame = grabber.grabImage()) != null) {
            if (frame.image == null) continue;

            Image image = converter.convert(frame);
            int windowWidth = getWidth();
            int windowHeight = getHeight();
            float scaleX = (float) width / windowWidth;
            float scaleY = (float) height / windowHeight;
            float maxScale = Math.max(scaleX, scaleY);
            ImageIcon icon = new ImageIcon(image.getScaledInstance((int) (width / maxScale), (int) (height / maxScale), Image.SCALE_SMOOTH));
            SwingUtilities.invokeLater(() -> videoLabel.setIcon(icon));

            Thread.sleep(40); // 控制帧率
        }

        grabber.stop();
    }

    @Override
    public void run() {
        try {
            setVisible(true);
            playVideo();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        String mp4Url = "G:\\视频\\动漫\\长安三万里2023.mp4";
        new Thread(new VideoPlayer("JavaCV Video Player", mp4Url)).start();
    }
}

2.3 grab() 获取音视频帧

将上面组合处理,但存在同步卡顿问题,音频尤其严重,建议音视频不同线程处理。

package org.xhbruce.test;

import org.bytedeco.ffmpeg.avcodec.AVCodecParameters;
import org.bytedeco.ffmpeg.avformat.AVFormatContext;
import org.bytedeco.ffmpeg.avformat.AVStream;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.xhbruce.Log.XLog;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;

public class SimpleFFPlay extends JFrame implements Runnable {
    private String filePath;
    private JLabel videoLabel;

    public SimpleFFPlay(String filePath) {
        this.filePath = filePath;
        setTitle("Simple FFPlay - JavaCV");
        setSize(800, 600);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        videoLabel = new JLabel();
        getContentPane().add(videoLabel, BorderLayout.CENTER);
        setVisible(true);
    }

    @Override
    public void run() {
        FFmpegFrameGrabber grabber = null;
        try {
            grabber = FFmpegFrameGrabber.createDefault(filePath);
        } catch (FFmpegFrameGrabber.Exception e) {
            throw new RuntimeException(e);
        }
        Java2DFrameConverter converter = new Java2DFrameConverter();

        try {
            grabber.start();
            //grabber.setFrameRate(30);
            // 音频播放初始化
            int sampleRate = grabber.getSampleRate();
            int audioChannels = grabber.getAudioChannels();
            int audioBitsPerSample = getAudioBitsPerSample(grabber);
            XLog.d("sampleRate="+sampleRate+", audioChannels="+audioChannels+", audioBitsPerSample=" + audioBitsPerSample
                    +"; getAudioBitrate="+grabber.getAudioBitrate());
            if (audioBitsPerSample <= 0) {
                audioBitsPerSample = 16; // 默认使用16位深度
            }

            AudioFormat audioFormat = new AudioFormat(
                    (float) sampleRate,
                    audioBitsPerSample/*grabber.getAudioBitsPerSample()*/,
                    audioChannels,
                    true,
                    false
            );

            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, 4096);
            SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info);
            //SourceDataLine audioLine = AudioSystem.getSourceDataLine(audioFormat);
            audioLine.open(audioFormat);
            audioLine.start();

            // 主循环读取帧
            Frame frame;
            while ((frame = grabber.grab()) != null) {
                if (frame.image != null) {
                    // 视频帧
                    BufferedImage image = converter.getBufferedImage(frame);
                    int width = grabber.getImageWidth();
                    int height = grabber.getImageHeight();
                    paintFrame(image, width, height);
                }
                if (frame.samples != null && frame.samples.length > 0) {
                    Buffer buffer = frame.samples[0];
                    byte[] audioBytes;

                    if (buffer instanceof ShortBuffer) {
                        ShortBuffer shortBuffer = (ShortBuffer) buffer;
                        audioBytes = new byte[shortBuffer.remaining() * 2]; // Each short is 2 bytes
                        for (int i = 0; i < shortBuffer.remaining(); i++) {
                            short value = shortBuffer.get(i);
                            audioBytes[i * 2] = (byte) (value & 0xFF);
                            audioBytes[i * 2 + 1] = (byte) ((value >> 8) & 0xFF);
                        }
                    } else if (buffer instanceof ByteBuffer) {
                        ByteBuffer byteBuffer = (ByteBuffer) buffer;
                        audioBytes = new byte[byteBuffer.remaining()];
                        byteBuffer.get(audioBytes);
                    } else {
                        throw new IllegalArgumentException("Unsupported buffer type: " + buffer.getClass());
                    }
                    audioLine.write(audioBytes, 0, audioBytes.length);
                }
                Thread.sleep(40); // 控制帧率
            }

            audioLine.drain();
            audioLine.stop();
            audioLine.close();
            grabber.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private int getAudioBitsPerSample(FFmpegFrameGrabber grabber) {
        AVFormatContext formatContext = grabber.getFormatContext();
        if (formatContext == null) {
            return -1;
        }

        int nbStreams = formatContext.nb_streams();
        for (int i = 0; i < nbStreams; i++) {
            AVStream stream = formatContext.streams(i);
            if (stream.codecpar().codec_type() == avutil.AVMEDIA_TYPE_AUDIO) {
                AVCodecParameters codecParams = stream.codecpar();
                return codecParams.bits_per_raw_sample();
            }
        }

        return -1;
    }

    private void paintFrame(BufferedImage image, int width, int height) {
        int windowWidth = getWidth();
        int windowHeight = getHeight();

        float scaleX = (float) width / windowWidth;
        float scaleY = (float) height / windowHeight;
        float maxScale = Math.max(scaleX, scaleY);
        //XLog.d("width="+width+", height="+height+"; windowWidth="+windowWidth+", windowHeight="+windowHeight+"; scaleX="+scaleX+", "+scaleY);

        ImageIcon icon = new ImageIcon(image.getScaledInstance((int) (width/maxScale), (int) (height/maxScale), Image.SCALE_SMOOTH));
        SwingUtilities.invokeLater(() -> {
//            setSize(800, 600);
            videoLabel.setIcon(icon);
        });
    }

    public static void main(String[] args) {
        if (args.length < 1) {
            System.out.println("Usage: java SimpleFFPlay <video_file>");
            return;
        }
        new Thread(new SimpleFFPlay(args[0])).start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xhBruce

佛系随缘,共同探讨

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值