如题,今天我们要开始设计一个视频播放器(当然它也能播放音乐),主要用来熟悉 Spring Boot
。
既然是冲着 Spring Boot
去的,语言必然用 Java
;核心的视频播放功能
则选择 vlcj,一个 libvlc
(你应该知道大名鼎鼎的 VLC 播放器 吧)的 Java
绑定库;Java
的 GUI
框架毫无疑问只能选 JavaFX
,而根据 vlcj
所述,
JavaFX 13
引入的原生内存缓冲(native memory buffer
) PixelBuffer
可以减少内存拷贝以提高性能,
所以我们一步到位,选用最新的 JavaFX 16
配合 Java 11
;原生的 GUI
主题难以令人满意,
所以我们引入 Win10
风的 JavaFX
主题 JMetro。
我们使用 gradle
来管理项目,构建 build.gradle
如下:
import java.util.stream.Collectors
plugins {
id 'java'
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
javafx {
version = '16'
modules = ['base', 'controls', 'fxml', 'graphics', 'media', 'swing', 'web'].stream().map {
'javafx.' + it
}.collect(Collectors.toList())
}
dependencies {
implementation group: 'org.springframework.boot', name: 'spring-boot-starter', version: '2.5.0'
['base', 'controls', 'fxml', 'graphics', 'media', 'swing', 'web'].each {
implementation group: 'org.openjfx', name: 'javafx-' + it, version: '16', classifier: 'win'
}
implementation group: 'org.jfxtras', name: 'jmetro', version: '11.6.15'
implementation group: 'uk.co.caprica', name: 'vlcj', version: '4.7.1'
implementation group: 'uk.co.caprica', name: 'vlcj-javafx', version: '1.0.2'
// Use JUnit Jupiter API for testing.
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.1'
// Use JUnit Jupiter Engine for testing.
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
// This dependency is used by the application.
implementation 'com.google.guava:guava:30.0-jre'
}
application {
// Define the main class for the application.
mainClass = 'fun.mactavish.mplayer.App'
}
tasks.named('test') {
// Use junit platform for unit tests.
useJUnitPlatform()
}
JavaFX
有好几个依赖,它们仅仅是 name 不同(但 name 还有着相同的前缀),所以我们用 Groovy
的高阶函数语法简化它们的引入。
但是仅仅引入依赖还不够,因为 JavaFX
使用 Java 9 Platform Module System
,
所以我们还要按文档要求引入 gradle
插件 'org.openjfx.javafxplugin'
并进行配置。
为了在 vlcj
基础上得到 PixelBuffer
支持,我们还需在 'uk.co.caprica:vlcj'
基础上引入 'uk.co.caprica:vlcj-javafx'
依赖。
接下来让我们写个 demo 验证一下一切正常。我们写一个引导类来初始化 Spring Boot
及 JavaFX
。
// Launcher.java
package fun.mactavish.mplayer;
import javafx.application.Application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Launcher {
public static void main(String[] args) {
SpringApplication.run(Launcher.class, args);
Application.launch(App.class, args);
}
}
SpringApplication
的静态方法 run
接收一个带有 @SpringBootApplication
注解的类的类对象及命令行参数,
它会完成 Spring Boot
的初始化任务(解析配置参数、扫描组件、创建 Bean
、注入依赖等)并返回一个类型为 ConfigurableApplicationContext
的
句柄,通过这个句柄可以读取配置参数、获取创建好的 Bean
等;
而 Application
的静态方法 launch
接收一个继承自 javafx.application.Application
的类的类对象及命令行参数,
它会初始化 JavaFX
并创建相关线程。
// App.java
package fun.mactavish.mplayer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import jfxtras.styles.jmetro.JMetro;
import jfxtras.styles.jmetro.Style;
public class App extends Application {
public App() {
}
@Override
public void start(Stage primaryStage) {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
Label l = new Label("Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".");
Scene scene = new Scene(new StackPane(l), 640, 480);
new JMetro(Style.LIGHT).setScene(scene);
primaryStage.setScene(scene);
primaryStage.show();
}
}
JavaFX
要求启动类不仅继承自 javafx.application.Application
,还以 public
修饰自身且有 public
的无参构造函数。
在 start
方法中我们可以决定 JavaFX
初始化后干什么,这里我们先创建一个简单的 Hello World 界面。
其中仅需 new JMetro(Style.LIGHT).setScene(scene);
这么简单的一行,我们就引入了 JMetro
主题。
通过 gradle run
启动项目,应当可以看到一个 GUI
的欢迎界面,同时终端输出 Spring Boot
日志,关闭界面窗口程序即退出。
这是我们软件实现的基点。
按照 vlcj-javafx 文档的指示,我们只需引入一个新的静态方法,即可将视频播放器装载在 JavaFX
的组件 ImageView
上:
// ...
import static uk.co.caprica.vlcj.javafx.videosurface.ImageViewVideoSurfaceFactory.videoSurfaceForImageView;
// ...
var factory = new MediaPlayerFactory();
var player = factory.mediaPlayers().newEmbeddedMediaPlayer();
var videoImageView = new ImageView();
player.videoSurface().set(videoSurfaceForImageView(videoImageView));
// ...
vlcj
会把视频渲染在这个 ImageView
上,而我们可以通过改变 ImageView
的属性(长宽等)来调整视频播放器界面的属性。
var root = new BorderPane();
root.setCenter(videoImageView);
videoImageView.fitWidthProperty().bind(root.widthProperty());
videoImageView.fitHeightProperty().bind(root.heightProperty());
Scene scene = new Scene(root, 1920, 1080);
我们用一个 BorderPane
来承载播放器界面,并将其的长宽与界面的长宽绑定。如此一来,用户调整窗口大小时,
界面大小会随之变化。现在启动程序,取代欢迎界面的将是一块黑幕,这是播放器未播放任何视频时的样子。我们可以通过
mediaPlayer.media().play(url);
来播放字符串 url
指定的媒体文件。
我们可以通过 Spring Boot
的值注入特性来从配置文件中读取 url
的值。创建文件 application.properties
并写下
file=E:\\npubits\\Yes.Prime.Minister.COMPLETE.PACK.DVD.x264-P2P\\Yes.Prime.Minister.S01.DVDRip.X264-BTN\\01.mkv
file
属性指向我本地磁盘上的一个 mkv 文件。在类 App
上添加注解 @Component
并增加字段
@Value("${file}")
private String url;
理论上,程序启动后 Spring Boot
将解析 application.properties
文件并将值注入 url
字段。但是事实并非如此。
进入 debug 模式可以发现 url
其实还为 null,但是用 IDEA 的 Spring
选项卡查看,发现 App Bean
已被生成且字段已被注入值。
这是怎么回事?为什么两者相冲突呢?原来,JavaFX
的启动方法 Application.launch(App.class, args);
自己会用反射创建 App
单例,
并通过那个单例远行程序,它与 Spring Boot
自己在容器中创建的 Bean
是两个不同的对象。debug 时看到的 App
对象是 JavaFX
创建的,
自然没有值注入;Spring
选项卡中的 App
对象虽然有值注入,但不通过 JavaFX
静态方法处理就没有用。
既然 JavaFX
非要负责创建 App
对象,我们只能在它上面放弃 Spring Boot
的特性;这问题也不大,毕竟它是我们的核心类。
但是 App
单例必须能得到对 Spring Boot
的控制权(即 ApplicationContext
)。于是,我们删除 Launcher
类,
转而在 App
对象的 init
方法中初始化 Spring Boot
并通过字段控制 ApplicationContext
。
@SpringBootApplication
public class App extends Application {
public App() {
}
private ConfigurableApplicationContext context;
public static void main(String[] commandLineArgs) {
// launch JavaFX
launch(App.class, commandLineArgs);
}
@Override
public void init() {
// initialize Spring Boot
String[] commandLineArgs = getParameters().getRaw().toArray(new String[0]);
this.context = SpringApplication.run(App.class, commandLineArgs);
}
@Override
public void start(Stage primaryStage) {
var root = new BorderPane();
var videoImageView = new ImageView();
root.setCenter(videoImageView);
videoImageView.fitWidthProperty().bind(root.widthProperty());
videoImageView.fitHeightProperty().bind(root.heightProperty());
Scene scene = new Scene(root, 1920, 1080);
// set JMetro theme
new JMetro(Style.LIGHT).setScene(scene);
// mount media player
var factory = new MediaPlayerFactory();
var mediaPlayer = factory.mediaPlayers().newEmbeddedMediaPlayer();
mediaPlayer.videoSurface().set(videoSurfaceForImageView(videoImageView));
primaryStage.setTitle("MPlayer");
primaryStage.setScene(scene);
primaryStage.show();
// play media whose url comes from properties
mediaPlayer.media().play(Objects.requireNonNull(context.getEnvironment().getProperty("file")));
}
}
注意,现在我们可以通过 ApplicationContext
来显式地获取配置文件中的属性:
mediaPlayer.media().play(Objects.requireNonNull(context.getEnvironment().getProperty("file")));
运行一下,还是失败退出了,这又是怎么了?根据 vlcj
文档所述,原来是 mediaPlayer
的生命期问题。
Java
的 GC
会销毁方法局部变量 mediaPlayer
对象,导致播放器 crash
。我们可以通过 App
单例的字段保存其引用,
以维持其生命期。但是有个更好的方案。我们可以用 Spring Boot
来管理它的生命期。让对象由容器创建,容器确保其始终存活;
同时我们能为其注册销毁方法,让容器在销毁 Bean
前先调用其 release
方法,释放相关联的原生资源(虽然程序退出时,
操作系统自己也会清理好)。对于无法修改源码的第三方对象,我们通过 @Configuration
和 @Bean
组合来注册 Bean
:
@Configuration
class VlcjConfiguration {
@Bean(destroyMethod = "release") // player.release() will be called on destroying
EmbeddedMediaPlayer mediaPlayer() {
var factory = new MediaPlayerFactory();
var player = factory.mediaPlayers().newEmbeddedMediaPlayer();
factory.release(); // release the factory
return player;
}
}
现在我们通过 context
来获取 mediaPlayer
对象:
// ...
// mount media player
EmbeddedMediaPlayer mediaPlayer = this.context.getBean(EmbeddedMediaPlayer.class);
mediaPlayer.videoSurface().set(videoSurfaceForImageView(videoImageView));
// ...
启动!一切 OK!
本文代码在 https://github.com/gonearewe/MPlayer/tree/a351957592f7dff8dc99d609154ed41767e4417a 可以找到
vlcj 自己提供了一个基于 Swing 的播放器范例,叫做 vlcj-player,使用到了主要的功能,也可供参考
如果你喜欢我的文章,请我吃根冰棒吧 (o゜▽゜)o ☆
最后附上 GitHub:https://github.com/gonearewe