Skip to content

视频 ​

ui.Video 显示一个正在播放的视频:在内容背后循环播放的短片,比如启动器的 媒体库或音乐播放器的头图展示的那样,也可以是网格中的预览。它显示一个 ui.VideoPlayer,由它播放解码器给出的帧,其布局、适配和圆角与图像 相同。

go
//go:embed waves.gif
var wavesGIF []byte

d, err := ui.NewGIFDecoder(wavesGIF)
if err != nil {
	log.Fatal(err)
}
app.waves, err = ui.NewVideoPlayer(d)
if err != nil {
	log.Fatal(err)
}
app.waves.SetLoop(true)

// 在视图里:
ui.Video(c, app.waves).Fill().Fit(ui.Cover)

视频示例 在窗口 内容背后播放一段短片。

播放器 ​

ui.NewVideoPlayer 会立即解码第一帧,Video 从第一帧开始显示它,同时也能 报告解码失败的视频,所以要像位图一样把播放器创建一次,不要在视图里创建。 新的播放器一旦被某个 Video 显示就开始播放。Pause 把它停在当前显示的帧, Play 从那里继续,或在播放结束后从头再播;Playing 说明当前状态。SetLoop 让它在每次结束后无缝重播。结束的视频显示最后一帧;Err 说明是什么让它在 结束前停止。Close 停止解码并关闭解码器(当它是 io.Closer 时)。在主线程 调用播放器的方法——在视图或输入处理器里——并且一次只在一个 Video 中显示 它。

go
label := "Pause"
if !app.waves.Playing() {
	label = "Play"
}
if ui.Button(c.Key("play"), label).Clicked() {
	if app.waves.Playing() {
		app.waves.Pause()
	} else {
		app.waves.Play()
	}
}

视频没有声音。

播放 ​

播放器在自己的 goroutine 上解码帧,领先当前帧约两帧,解码进可复用的帧; 每个帧到点后通过位图的 Update 到达屏幕:视频每有一帧, 窗口就绘制一帧(由 After 请求),期间不多画。当解码跟不上时,窗口会等待 而不绘制,而期间到点的帧只显示最后一个,这样视频跟着时钟走,而不是落后。

视频只在视图构建它且窗口部分可见时才继续播放。移出视图的(比如在另一个 标签页),或在最小化、隐藏、被其他窗口遮挡的窗口里的(在 macOS,以及在 浏览器中),再次显示时从停下的位置继续,期间不解码任何帧。当桌面要求减少 动态效果时(Preferences.ReduceMotion),视频只显示第一帧而不播放;桌面不再 要求后,从停下的位置继续。

解码器 ​

ui.NewGIFDecoder 用 Go 解码动画 GIF,显示方式与浏览器相同:每帧按照其 处置(disposal)方式画在前面各帧留下的内容之上,10 毫秒或更短的帧按 100 毫秒显示。GIF 一次性解码进内存,每帧每像素一字节,播放时逐帧绘制。无论 GIF 自带的循环次数是多少,它都按 SetLoop 的设定播放一次或循环。

自己实现的 ui.VideoDecoder 可以播放其他格式:Next 返回下一帧——任意 图像类型的画面及其显示时长,最后一帧之后返回 io.EOF;Rewind 回到第一帧 之前。播放器会复制每个画面,因此解码器可以把所有帧画进同一个图像,并且 一次只从一个 goroutine 调用解码器。

go
type frames struct {
	pics []*image.RGBA
	next int
}

func (f *frames) Next() (ui.VideoFrame, error) {
	if f.next == len(f.pics) {
		return ui.VideoFrame{}, io.EOF
	}
	f.next++
	return ui.VideoFrame{Image: f.pics[f.next-1], Duration: time.Second / 30}, nil
}

func (f *frames) Rewind() error { f.next = 0; return nil }

基于系统编解码器(H.264、HEVC、VP9 与 AV1,经由 Media Foundation、 AVFoundation 和 GStreamer)的解码器尚未纳入 MyGo。

无障碍 ​

和图像一样,视频是辅助技术看不到的装饰,除非用 Label 为它命名。