Java使用javacv实现的多种音视频格式播放器
一、前言
最近写了一款图形界面版的音视频播放器,可以支持多种音视频格式的播放,比如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)); // 设置边距为0playPauseButton.setBorderPainted(false);playPauseButton.setContentAreaFilled(false); // 禁用背景填充(透明背景)playPauseButton.setFocusPainted(false); // 移除焦点框(可选)playPauseButton.setMargin(new Insets(0, 0, 0, 0)); // 设置边距为0fullExitScreen=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() {@Overridepublic 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,高度为20progressBar.setPreferredSize(preferredSize);progressBar.setUI(new CustomSliderUI(progressBar));progressBar.setFocusable(false); // 禁用焦点绘制// progressBar.setPaintTicks(true);//progressBar.setPaintLabels(true);progressBar.addMouseListener(new MouseAdapter() {@Overridepublic void mousePressed(MouseEvent e) {userDragging=true;//if (!progressBar.getValueIsAdjusting()) {BasicSliderUI ui = (BasicSliderUI) progressBar.getUI();int value = ui.valueForXPosition(e.getX());progressBar.setValue(value);// }}@Overridepublic 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);// 初始化 FFmpegFrameGrabbertry {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实现音视频播放器源码