一、前言
最近写了一款图形界面版的音视频播放器,可以支持多种音视频格式的播放,比如MP4、avi、mkv、flv、MP3、ogg、wav等多种格式,非常好用,可以本地打开多种格式音视频。

二、实现
1.通过引入javacv相关依赖实现,如下:

 <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>1.5.9</version>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.9</version>
        </dependency>

2.定义一个类VideoPlayer,实现播放功能,代码如下:

public class VideoPlayer {

    private FFmpegFrameGrabber grabber;
    private CanvasFrame canvasFrame;
    private volatile boolean isPlaying = true; // 控制播放/暂停状态
    private volatile boolean isScreen = false; // 放大至整个屏幕
    private Thread playbackThread;
    private JSlider progressBar;
    private JButton playPauseButton; //播放或暂停按钮
    private JButton openFileButton;
    private JButton fullExitScreen;
    private ImageIcon playImage;
    private ImageIcon pauseImage;
    private ImageIcon fullScreenImage;
    private ImageIcon exitScreenImage;
    private JLabel startTimeJl;
    private JLabel durationJl;
    private long totalDuration; // 视频总时长(毫秒)
    private long maxTimestampUs;//最大帧时长(微秒)
    private volatile long currentTime = 0; // 当前播放时间(毫秒)
	private volatile long lastProgressUpdate = 0; // 上次更新进度条的时间
    private volatile boolean isSelectFile = false; // 上次更新进度条的时间
    private volatile boolean userDragging = false;
    private String videoPath;
	private static final long PROGRESS_UPDATE_INTERVAL = 1000; // 每秒更新一次进度条
    private ReentrantLock lock = new ReentrantLock();

    // 使用一个队列来缓存已解码的帧
    LinkedHashMap<Integer,Frame> frameCacheMap = new LinkedHashMap<>();

    public VideoPlayer(String videoPath,String title) {
        // 初始化窗口
        // 创建画布,用于显示帧
        this.videoPath=videoPath;
        canvasFrame = new CanvasFrame(title,1.00); // 创建画布,第二个参数为窗口的持续时间系数,1.0表示实时
        canvasFrame.getRootPane().setWindowDecorationStyle(JRootPane.NONE);

        canvasFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        //canvasFrame.setLocationRelativeTo(null);
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

        int x = (screenSize.width - 1000) / 2;
        int y = (screenSize.height - 800) / 2;

        canvasFrame.setLocation(x, y);
        canvasFrame.setCanvasSize(1000,700);
        canvasFrame.setSize(1000,800);

        //canvasFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
        //setFullScreen();
        canvasFrame.setVisible(true);
        // 创建一个画布用于显示视频帧
        canvasFrame.setResizable(false);
        // 创建播放/暂停按钮
        playPauseButton = new JButton();

        playPauseButton.setMargin(new Insets(0, 0, 0, 0)); // 设置边距为0
        playPauseButton.setBorderPainted(false);
        playPauseButton.setContentAreaFilled(false);    // 禁用背景填充(透明背景)
        playPauseButton.setFocusPainted(false);  // 移除焦点框(可选)
        playPauseButton.setMargin(new Insets(0, 0, 0, 0)); // 设置边距为0

        fullExitScreen=new JButton();
        fullExitScreen.setBorderPainted(false);
        fullExitScreen.setContentAreaFilled(false);    // 禁用背景填充(透明背景)
        fullExitScreen.setFocusPainted(false);  // 移除焦点框(可选)

        openFileButton=new JButton("打开文件");
        openFileButton.setFocusPainted(false);
        JPanel jFilePanel =new JPanel();
        jFilePanel.setOpaque(false);
        jFilePanel.setBackground(null);
        jFilePanel.setPreferredSize(new Dimension(100, 30));
        openFileButton.setPreferredSize(new Dimension(90, 30));
        jFilePanel.add(openFileButton);
        // 获取类加载器
        ClassLoader classLoader = VideoPlayer.class.getClassLoader();
        playImage = new ImageIcon(classLoader.getResource("image/play.png"));
        pauseImage = new ImageIcon(classLoader.getResource("image/pause.png"));

        fullScreenImage = new ImageIcon(classLoader.getResource("image/full.png"));
        exitScreenImage = new ImageIcon(classLoader.getResource("image/exitFull.png"));
        fullExitScreen.setIcon(fullScreenImage);
        playPauseButton.setIcon(pauseImage);
        playPauseButton.addActionListener(e -> togglePlayback());

        fullExitScreen.addActionListener(e -> toggleFullScreen());

        openFileButton.addMouseListener(
                new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent e) {
                        openFile();
                    }
                });
       // canvasFrame.add(playPauseButton, BorderLayout.SOUTH);
        startTimeJl = new JLabel("00:00");
        durationJl = new JLabel();
        // 创建进度条
        progressBar = new JSlider(0, 100, 0); // 初始范围为 0-100%
        progressBar.setMajorTickSpacing(10);
        progressBar.setMinorTickSpacing(1);
        progressBar.setSnapToTicks(true);
        // 设置 JSlider 的首选大小
        Dimension preferredSize = new Dimension(850, 20); // 宽度为300,高度为20
        progressBar.setPreferredSize(preferredSize);
        progressBar.setUI(new CustomSliderUI(progressBar));
        progressBar.setFocusable(false); // 禁用焦点绘制
        // progressBar.setPaintTicks(true);
        //progressBar.setPaintLabels(true);
        progressBar.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                userDragging=true;
                //if (!progressBar.getValueIsAdjusting()) {
                    BasicSliderUI ui = (BasicSliderUI) progressBar.getUI();
                    int value = ui.valueForXPosition(e.getX());
                    progressBar.setValue(value);
               // }

            }
            @Override
            public void mouseReleased(MouseEvent e) {
                userDragging = false;
            }
        });
        // canvasFrame.setCanvasSize(800,500);
        progressBar.addChangeListener(e -> {
            try {
                if (!isSelectFile) {
                    int position = progressBar.getValue();
                    //注释表示这个控制拖动中不生效,拖动完成才生效
                    //if(!progressBar.getValueIsAdjusting() || position==0 || position==100){
                    if (!isSelectFile && position==100) {
                        seekToPosition();
                    }
                    Thread.sleep(5);
                    seekToPosition();
                    // }
                }
            } catch (FFmpegFrameGrabber.Exception | InterruptedException ex) {
                ex.printStackTrace();
                return;
            }
        }); // 监听进度条变化
        Canvas canvas=canvasFrame.getCanvas();
        canvas.setBounds(0,0,1000,700);
        JPanel jPanel =new JPanel();
        JPanel jBarPanel =new JPanel();
        jBarPanel.setOpaque(false);
        jBarPanel.setBackground(null);
        jBarPanel.setPreferredSize(new Dimension(1000, 25));
        jPanel.setOpaque(false);
        jPanel.setVisible(true);
        jPanel.setBounds(0,700,1000,100);
        jPanel.setOpaque(false);
        jPanel.setVisible(true);
        jPanel.setBackground(null);
        jPanel.setLayout(new BorderLayout());
        jBarPanel.add(startTimeJl);
        jBarPanel.add(progressBar);
        jBarPanel.add(durationJl);
        jPanel.add(jBarPanel,BorderLayout.NORTH);
        jPanel.add(playPauseButton,BorderLayout.CENTER);
        jPanel.add(jFilePanel,BorderLayout.WEST);
        jPanel.add(fullExitScreen,BorderLayout.EAST);
        jPanel.setPreferredSize(new Dimension(1000, 100));
        canvasFrame.add(jPanel,BorderLayout.SOUTH);

        // 初始化 FFmpegFrameGrabber
        try {
            maxTimestampUs = getMaxTimestampUs(videoPath);
            grabber = new FFmpegFrameGrabber(videoPath);
            grabber.start();
            setGrabberParam(grabber,videoPath.endsWith(".ts"));
            // 获取视频的总时长(毫秒)
            totalDuration = (long) grabber.getLengthInTime() / 1000;
            durationJl.setText(convertMilliseconds(totalDuration));
        } catch (Exception e) {
            e.printStackTrace();
            JOptionPane.showMessageDialog(null, "打开文件时发生错误。");
            return;
        }

        // 启动播放线程
        startPlayback();
    }
    private void startPlayback() {
        playbackThread = new Thread(() -> {
            int audioStreamIndex = -1;
            AudioPlayer audioPlayer=null;
            // if (!grabber.getAudioCodecName().contains("mp3") && !grabber.getAudioCodecName().contains("aac")) {
            // 获取音频流信息
            audioPlayer = new AudioPlayer(grabber.getSampleRate(),16,grabber.getAudioChannels());
            //  }
            if (audioStreamIndex == -1) {
                System.err.println("No audio stream found.");
            }
            while (true) {
                if (!isPlaying) {
                    // 暂停时,线程休眠
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                // 抓取下一帧
                Frame frame = null;
				// 定期更新进度条
                if (!lock.isLocked()) {
                    try {
                        if (grabber.getTimestamp()<=0 && grabber.getVideoCodec()<=0) {
                            grabber.setTimestamp(0);
                           // grabber.start();
                        }
                        if (!isSelectFile && progressBar.getValue()==100) {
                            isPlaying=true;
                            //grabber.setTimestamp(grabber.getTimestamp());
                        }
                        frame = grabber.grabFrame();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
             ......................................

3.实现的打开文件选择功能。

private void openFile() {
        if (!openFileButton.isEnabled()) {
            return;
        }
        String uiClassName = UIManager.getLookAndFeel().getClass().getName();
        setLookAndFeel(null);
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
        List<String> allowList = new ArrayList<>(Arrays.asList("mp4", "mkv", "avi", "wmv","ts","flv","mp3", "wav", "ogg", "wma","aac","au","ac3","m4a"));
        String[] allowArray = allowList.stream().toArray(String[]::new);
        FileFilter fileFilter = new FileNameExtensionFilter("文件", allowArray);

        fileChooser.setFileFilter(fileFilter);
        fileChooser.addChoosableFileFilter(fileFilter);
        int returnVal = fileChooser.showOpenDialog(null);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            File file = fileChooser.getSelectedFile();
            try {
                isPlaying=false;
                isSelectFile=true;
                grabber.setTimestamp(0);
                grabber.stop();
                grabber.close();
                grabber.setAudioChannels(0);
                grabber.setVideoCodec(0);
                isSelectFile=true;
                SwingUtilities.invokeLater(() -> {
                    int progress = progressBar.getValue();
                    progressBar.setValue(0);
                    //BoundedRangeModel model = progressBar.getModel();
                   // model.setValue(0);  // 直接更新模型
                    if (progress==100) {
                        progressBar.setUI(new CustomSliderUI(progressBar));
                        progressBar.repaint();
                    }
                    lastProgressUpdate=0;
                    maxTimestampUs = getMaxTimestampUs(file.getAbsolutePath());
                    if (maxTimestampUs!=-2) {
                        grabber = new FFmpegFrameGrabber(file.getAbsolutePath());
                        try {
                            grabber.start();
                        } catch (FFmpegFrameGrabber.Exception e) {
                            e.printStackTrace();
                        }
                        this.videoPath=file.getAbsolutePath();
                        canvasFrame.setTitle("播放-"+file.getName());
                        setGrabberParam(grabber,file.getAbsolutePath().endsWith(".ts"));
                        // 获取视频的总时长(毫秒)
                        totalDuration = (long) grabber.getLengthInTime() / 1000;
                        startTimeJl.setText("00:00");
                        durationJl.setText(convertMilliseconds(totalDuration));
                        isSelectFile=false;
                        isPlaying=true;
                        Graphics g = canvasFrame.getCanvas().getGraphics();
                        g.clearRect(0, 0, canvasFrame.getCanvas().getWidth(), canvasFrame.getCanvas().getHeight());
                        playPauseButton.setIcon(pauseImage);
                    }
                });

            } catch (Exception e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(null, "打开文件时发生错误。");
            }
        }
        setLookAndFeel(uiClassName);
    }

4.最后通过main方法启动。

public static void main(String[] args) throws UnsupportedEncodingException {
        String videoPath = URLDecoder.decode(VideoPlayer.class.getResource("/video/赤伶.mp4").getPath(),"utf-8");
        if (videoPath.startsWith("/")) {
            videoPath = videoPath.substring(1);
        }
        // 创建并显示窗口
        String finalVideoPath = videoPath;
        SwingUtilities.invokeLater(() -> {
			VideoPlayer player = new VideoPlayer(finalVideoPath,"播放-赤伶.mp4");
        });
    }

启动后效果如下:
在这里插入图片描述
放大后效果:
在这里插入图片描述
完整代码如下:
javacv实现音视频播放器源码

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐