主题
在你的 Flet 应用中嵌入功能完整的视频播放器,支持播放列表、硬件加速控制和字幕配置。
它由 Flutter 的 media_kit 包提供支持。
平台支持
| 平台 | Windows | macOS | Linux | iOS | Android | Web |
|---|---|---|---|---|---|---|
| 支持 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
用法
把 flet-video 包添加到项目依赖中:
uv
bash
uv add flet-videopip
bash
pip install flet-video [^1][^1]: 之后你需要手动把这个包添加到 requirements.txt 或 pyproject.toml 中。
环境要求
以下各节展示了各平台所需的配置。
Android
不需要任何权限即可播放来自 URL、打包的静态资源,或用户通过 FilePicker 选取的文件中的媒体 —— 系统选择器会授予对所选取文件的访问权。
只有当你的应用直接从设备共享媒体库读取媒体(而不是让用户逐个选择文件)时,才需要媒体权限:
android.permission.READ_MEDIA_AUDIO:允许从外部存储读取音频文件。Android 13 或更高版本。android.permission.READ_MEDIA_VIDEO:允许从外部存储读取视频文件。Android 13 或更高版本。android.permission.READ_EXTERNAL_STORAGE:允许从外部存储读取。Android 12 或更低版本 —— 通过maxSdkVersion绑定,使其在 Android 13 或更高版本上不会被请求。
Google Play 政策
Google Play 的照片和视频权限政策会拒绝目标为 Android 13(API 33)或更高版本、且声明了 READ_MEDIA_IMAGES 或 READ_MEDIA_VIDEO 而系统选择器已足够使用的应用。 在任何生效的 version code 中声明它们 —— 包括内部测试和封闭测试 轨道 —— 都足以阻止发布。
除非你确实需要宽泛的媒体库访问权限,否则请使用 FilePicker; 若确实需要,你还必须在 Play Console 中提交照片选择器声明 表。
bash
flet build apk \
--android-permissions android.permission.READ_MEDIA_AUDIO=true \
--android-permissions android.permission.READ_MEDIA_VIDEO=truetoml
[tool.flet.android.permission]
"android.permission.READ_MEDIA_AUDIO" = true
"android.permission.READ_MEDIA_VIDEO" = true
"android.permission.READ_EXTERNAL_STORAGE" = { maxSdkVersion = "32" }使用 PermissionHandler 在运行时请求权限。
另见:
Linux
libmpv 库必须已安装,并存在于运行应用的机器上。
在 Ubuntu/Debian 上,可以这样安装:
bash
sudo apt install libmpv-dev mpv如果遇到 libmpv.so.1 加载错误,请运行:
bash
sudo apt update
sudo apt install libmpv-dev libmpv2
sudo ln -s /usr/lib/x86_64-linux-gnu/libmpv.so /usr/lib/libmpv.so.1示例
python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
page.add(
ft.SafeArea(
expand=True,
content=ftv.Video(
expand=True,
autoplay=True,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
async def handle_play(e: ft.Event[ft.Button]):
await video.play()
async def handle_play_or_pause(e: ft.Event[ft.Button]):
await video.play_or_pause()
async def handle_pause(e: ft.Event[ft.Button]):
await video.pause()
async def handle_stop(e: ft.Event[ft.Button]):
await video.stop()
async def handle_seek(e: ft.Event[ft.Button]):
await video.seek(ft.Duration(seconds=10))
async def handle_next(e: ft.Event[ft.Button]):
await video.next()
async def handle_previous(e: ft.Event[ft.Button]):
await video.previous()
async def handle_status(e: ft.Event[ft.Button]):
playing = await video.is_playing()
position = (await video.get_current_position()).in_seconds
duration = (await video.get_duration()).in_seconds
page.show_dialog(
ft.SnackBar(
f"Playing?: {playing} | Duration: {duration}s | Position: {position}s"
)
)
page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
video := ftv.Video(
expand=True,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4"
),
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4"
),
],
),
ft.Row(
wrap=True,
alignment=ft.MainAxisAlignment.CENTER,
controls=[
ft.Button("Play", on_click=handle_play),
ft.Button("Play Or Pause", on_click=handle_play_or_pause),
ft.Button("Pause", on_click=handle_pause),
ft.Button("Stop", on_click=handle_stop),
ft.Button("Seek 10s", on_click=handle_seek),
ft.Button("Previous", on_click=handle_previous),
ft.Button("Next", on_click=handle_next),
ft.Button("Show Status", on_click=handle_status),
],
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
async def handle_screenshot(e: ft.Event[ft.FloatingActionButton]):
image = await video.take_screenshot(format="image/png")
if image is None:
status.value = "No video frame is available yet."
else:
preview.content = ft.Image(
src=image,
width=260,
height=146,
fit=ft.BoxFit.CONTAIN,
)
status.value = f"Captured {len(image)} bytes."
page.update()
page.floating_action_button = ft.FloatingActionButton(
content="Take screenshot",
icon=ft.Icons.CAMERA_ALT,
on_click=handle_screenshot,
)
page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
video := ftv.Video(
expand=True,
autoplay=True,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
],
),
preview := ft.Container(
width=260,
height=146,
alignment=ft.Alignment.CENTER,
bgcolor=ft.Colors.BLACK_12,
content=ft.Text("No screenshot yet"),
),
status := ft.Text(),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import random
import flet as ft
import flet_video as ftv
media = [
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4"
),
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373716-76da0a4e-225a-44e4-9ee7-3e9006dbc3e3.mp4"
),
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373695-22f88f13-d18f-4288-9bf1-c3e078d83722.mp4"
),
]
def main(page: ft.Page):
def handle_add(e: ft.Event[ft.Button]):
video.playlist.append(random.choice(media))
def handle_remove(e: ft.Event[ft.Button]):
if video.playlist:
video.playlist.pop(random.randint(0, len(video.playlist) - 1))
def handle_replace(e: ft.Event[ft.Button]):
video.playlist = random.sample(media, 3)
async def handle_next(e: ft.Event[ft.Button]):
await video.next()
async def handle_previous(e: ft.Event[ft.Button]):
await video.previous()
async def handle_jump(e: ft.Event[ft.Button]):
await video.jump_to(0)
page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
video := ftv.Video(
expand=True,
autoplay=True,
playlist=media[:2],
playlist_mode=ftv.PlaylistMode.LOOP,
),
ft.Row(
wrap=True,
alignment=ft.MainAxisAlignment.CENTER,
controls=[
ft.Button("Add Random", on_click=handle_add),
ft.Button("Remove Random", on_click=handle_remove),
ft.Button("Replace Playlist", on_click=handle_replace),
ft.Button("Next", on_click=handle_next),
ft.Button("Previous", on_click=handle_previous),
ft.Button("Jump to First", on_click=handle_jump),
],
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
def handle_load(e: ft.Event[ftv.Video]):
print("Video loaded")
def handle_complete(e: ft.Event[ftv.Video]):
print(f"Track completed: {e.data}")
def handle_track_change(e: ft.Event[ftv.Video]):
print(f"Track changed to index {e.data}")
def handle_position_change(e: ft.Event[ftv.Video]):
print(f"Position: {e.data.in_milliseconds / 1000:.3f}s")
def handle_duration_change(e: ft.Event[ftv.Video]):
print(f"Duration changed: {e.data.in_seconds}s")
def handle_error(e: ft.Event[ftv.Video]):
print(f"Error: {e.data}")
def handle_enter_fullscreen(e: ft.Event[ftv.Video]):
print("Entered fullscreen")
def handle_exit_fullscreen(e: ft.Event[ftv.Video]):
print("Exited fullscreen")
page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
controls=[
ftv.Video(
expand=True,
autoplay=True,
on_load=handle_load,
on_complete=handle_complete,
on_track_change=handle_track_change,
on_position_change=handle_position_change,
on_duration_change=handle_duration_change,
on_error=handle_error,
on_enter_fullscreen=handle_enter_fullscreen,
on_exit_fullscreen=handle_exit_fullscreen,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373718-86ce5e1d-d195-45d5-baa6-ef94041d0b90.mp4"
),
],
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
SAMPLE_VTT = """WEBVTT
00:00:00.000 --> 00:00:04.000
Welcome to flet-video!
00:00:04.000 --> 00:00:08.000
Subtitles are styled via VideoSubtitleConfiguration.
00:00:08.000 --> 00:00:14.000
This track is provided as raw VTT text.
"""
def main(page: ft.Page):
page.spacing = 20
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
subtitle_track = ftv.VideoSubtitleTrack(
src=SAMPLE_VTT,
title="English",
language="en",
)
def handle_change(e: ft.Event[ft.Switch]):
if e.control.value:
video.subtitle_track = subtitle_track
else:
video.subtitle_track = ftv.VideoSubtitleTrack.none()
page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
video := ftv.Video(
expand=True,
autoplay=False,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
],
subtitle_track=subtitle_track,
subtitle_configuration=ftv.VideoSubtitleConfiguration(
text_align=ft.TextAlign.CENTER,
text_style=ft.TextStyle(
size=24,
color=ft.Colors.YELLOW,
weight=ft.FontWeight.BOLD,
bgcolor=ft.Colors.BLACK_54,
),
),
),
ft.Switch(
value=True, label="Show Subtitles", on_change=handle_change
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
def set_controls(
value: ftv.VideoControls | ft.Control | None,
):
video.controls = value
page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
video := ftv.Video(
expand=True,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
],
controls=ftv.MaterialDesktopVideoControls(
visible_on_mount=True,
display_seek_bar=True,
modify_volume_on_scroll=True,
toggle_fullscreen_on_double_press=True,
play_and_pause_on_tap=True,
seek_bar_position_color=ft.Colors.BLUE,
volume_bar_active_color=ft.Colors.YELLOW,
controls_hover_duration=ft.Duration(seconds=5),
),
),
ft.Row(
wrap=True,
alignment=ft.MainAxisAlignment.CENTER,
controls=[
ft.Button(
"Adaptive",
on_click=lambda _: set_controls(
ftv.AdaptiveVideoControls(
material=ftv.MaterialVideoControls(
visible_on_mount=True,
display_seek_bar=True,
volume_gesture=True,
brightness_gesture=True,
seek_gesture=True,
seek_on_double_tap=True,
speed_up_on_long_press=True,
seek_bar_position_color=ft.Colors.BLUE,
button_bar_button_color="#E0F7FA",
),
material_desktop=ftv.MaterialDesktopVideoControls(
visible_on_mount=True,
display_seek_bar=True,
modify_volume_on_scroll=True,
toggle_fullscreen_on_double_press=True,
play_and_pause_on_tap=True,
hide_mouse_on_controls_removal=True,
seek_bar_position_color=ft.Colors.BLUE,
volume_bar_active_color=ft.Colors.YELLOW,
),
)
),
),
ft.Button(
"Material",
on_click=lambda _: set_controls(
ftv.MaterialVideoControls(
visible_on_mount=True,
volume_gesture=True,
brightness_gesture=True,
seek_gesture=True,
seek_on_double_tap=True,
speed_up_on_long_press=True,
speed_up_factor=2.5,
controls_transition_duration=ft.Duration(
milliseconds=450
),
seek_bar_position_color=ft.Colors.BLUE,
button_bar_button_color="#E0F7FA",
)
),
),
ft.Button(
"Material Desktop",
on_click=lambda _: set_controls(
ftv.MaterialDesktopVideoControls(
visible_on_mount=True,
display_seek_bar=True,
modify_volume_on_scroll=True,
toggle_fullscreen_on_double_press=True,
play_and_pause_on_tap=True,
hide_mouse_on_controls_removal=True,
controls_hover_duration=ft.Duration(seconds=5),
seek_bar_position_color=ft.Colors.BLUE,
seek_bar_hover_height=8,
volume_bar_active_color=ft.Colors.YELLOW,
)
),
),
ft.Button(
"Custom Control",
on_click=lambda _: set_controls(
ft.Container(
alignment=ft.Alignment.BOTTOM_CENTER,
padding=16,
content=ft.Text("This is a custom control..."),
)
),
),
ft.Button(
"No Controls",
on_click=lambda _: set_controls(None),
),
],
),
],
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
page.add(
ft.SafeArea(
expand=True,
content=ftv.Video(
expand=True,
playlist=[
ftv.VideoMedia(
"https://user-images.githubusercontent.com/28951144/229373720-14d69157-1a56-4a78-a2f4-d7a134d7c3e9.mp4"
),
],
controls={
ftv.VideoControlsMode.NORMAL: ftv.MaterialDesktopVideoControls(
visible_on_mount=True,
bottom_button_bar=[
ftv.VideoPlayOrPauseButton(),
ftv.VideoSpacer(),
ft.Text("NORMAL Mode", weight=ft.FontWeight.BOLD),
ftv.VideoFullscreenButton(),
],
),
ftv.VideoControlsMode.FULLSCREEN: ftv.MaterialDesktopVideoControls(
visible_on_mount=True,
bottom_button_bar=[
ftv.VideoFullscreenButton(),
ftv.VideoSpacer(),
ft.Text("FULLSCREEN Mode", weight=ft.FontWeight.BOLD),
ftv.VideoPlayOrPauseButton(),
],
),
},
),
)
)
if __name__ == "__main__":
ft.run(main)python
import flet as ft
import flet_video as ftv
def main(page: ft.Page):
label_style = ft.TextStyle(
size=12,
color=ft.Colors.WHITE,
weight=ft.FontWeight.BOLD,
)
page.add(
ft.SafeArea(
expand=True,
content=ftv.Video(
expand=True,
playlist=[
ftv.VideoMedia("video-sample.mp4"),
],
controls=ftv.MaterialDesktopVideoControls(
visible_on_mount=True,
primary_button_bar=[
ftv.VideoSkipPreviousButton(icon_color=ft.Colors.CYAN),
ftv.VideoPlayOrPauseButton(
icon_size=40,
icon_color=ft.Colors.CYAN,
),
ftv.VideoSkipNextButton(
icon=ft.Icon(ft.Icons.FAST_FORWARD),
icon_color=ft.Colors.CYAN,
),
],
top_button_bar=[
ft.Text("Top button bar", style=label_style),
ftv.VideoSpacer(),
ftv.VideoFullscreenButton(icon_color=ft.Colors.AMBER),
],
bottom_button_bar=[
ft.Text("Bottom button bar", style=label_style),
ftv.VideoSpacer(),
ftv.VideoPositionIndicator(
text_style=ft.TextStyle(
size=13,
color=ft.Colors.WHITE,
)
),
ftv.VideoVolumeButton(
slider_width=96,
icon_color=ft.Colors.AMBER,
),
],
),
),
)
)
if __name__ == "__main__":
ft.run(main)描述
Video 继承自:ft.LayoutControl, Control, BaseControl
一个从播放列表显示视频的控件。
属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
playlist | list[VideoMedia] | [] | 表示要播放的视频文件的 VideoMedia 列表。 |
title | str | 'flet-video' | 定义原生后端底层窗口和进程的名称。它会显示在系统窗口音量混合器中。 |
fit | ft.BoxFit | ft.BoxFit.CONTAIN | 用于视频的 box fit。 |
fill_color | ft.ColorValue | ft.Colors.BLACK | 定义用于填充视频背景的颜色。 |
wakelock | bool | True | 播放视频时是否获取唤醒锁。为 True 时,视频播放期间设备屏幕不会进入待机/休眠。 |
autoplay | bool | False | 视频是否应自动开始播放。 |
controls | Optional[Union[VideoControls, ft.Control, dict[VideoControlsMode, Optional[Union[VideoControls, ft.Control]]]]] | AdaptiveVideoControls(() | 显示在视频之上的控件。 设为 VideoControls 对象可使用内置控件,设为 Control 对象可使用自定义 Flet 控件,设为 None 则隐藏控件。 若要在全屏内外使用不同的控件,请把此属性设为一个以 VideoControlsMode 为键的字典。当未提供 NORMAL 控件时,会使用 DEFAULT 值。如果未提供 FULLSCREEN 控件,会先复用 NORMAL 控件,再回退到 DEFAULT。某个模式的值设为 None 时,仅对该模式隐藏控件。 |
fullscreen | bool | False | 视频播放器是否以全屏模式呈现。 设为 True 可进入全屏,设为 False 可通过编程方式退出全屏。 |
muted | bool | False | 定义视频播放器是否应以静音状态启动。 |
playlist_mode | Optional[PlaylistMode] | None | 表示播放列表的播放模式。 |
shuffle_playlist | bool | False | 定义是否应随机打乱播放列表。 |
volume | ft.Number | 100.0 | 定义视频播放器的音量。 注意: 其取值范围在 0.0 到 100.0(含)之间,其中 0.0 为静音,100.0 为最大音量。 如果取值超出该范围会抛出异常。 抛出异常: ValueError:如果它的值不在 0.0 到 100.0(含)之间。 |
playback_rate | ft.Number | 1.0 | 定义视频播放器的播放速率。 |
alignment | ft.Alignment | ft.Alignment.CENTER() | 定义视口的对齐方式。 |
filter_quality | ft.FilterQuality | ft.FilterQuality.LOW | 用于渲染视频输出的纹理的过滤质量。 注意: 据报告,Android 在使用 HIGH 时会显示模糊图像。在该平台上建议使用 MEDIUM。 |
pause_upon_entering_background_mode | bool | True | 应用进入后台模式时是否暂停视频。 |
resume_upon_entering_foreground_mode | bool | False | 应用进入前台模式时是否恢复视频。仅当 pause_upon_entering_background_mode 也设为 True 时才生效。 |
pitch | ft.Number | 1.0 | 定义视频播放器的相对音调。 |
configuration | VideoConfiguration | VideoConfiguration(() | 视频播放器的额外配置。 |
subtitle_configuration | VideoSubtitleConfiguration | VideoSubtitleConfiguration(() | 定义视频播放器的字幕配置。 |
subtitle_track | Optional[VideoSubtitleTrack] | None | 定义视频播放器的字幕轨道。 |
width | Optional[Number] | None | 控件的强制宽度(以虚拟像素为单位)。 |
height | Optional[Number] | None | 控件的强制高度(以虚拟像素为单位)。 |
left | Optional[Number] | None | 子控件左边缘相对于 Stack 左侧的内缩距离。 注意: 仅当此控件是以下之一的后代时才生效: Stack 控件、overlay 列表。 |
top | Optional[Number] | None | 子控件上边缘相对于 Stack 顶部的内缩距离。 注意: 仅当此控件是以下之一的后代时才生效: Stack 控件、overlay 列表。 |
right | Optional[Number] | None | 子控件右边缘相对于 Stack 右侧的内缩距离。 注意: 仅当此控件是以下之一的后代时才生效: Stack 控件、overlay 列表。 |
bottom | Optional[Number] | None | 子控件下边缘相对于 Stack 底部的内缩距离。 注意: 仅当此控件是以下之一的后代时才生效: Stack 控件、overlay 列表。 |
align | Optional[Alignment] | None | 控件在其父级中的对齐方式。 |
margin | Optional[MarginValue] | None | 设置控件的外边距。 |
rotate | Optional[RotateValue] | None | 围绕控件中心旋转来变换此控件。 rotate 属性的值可以是以下类型之一: * number —— 顺时针旋转的弧度值。整圈 360° 为 math.pi * 2 弧度,90° 为 pi / 2,45° 为 pi / 4,以此类推。 * Rotate —— 允许同时指定旋转 angle 和 alignment,即旋转中心的位置。 示例: python ft.Image( src="https://picsum.photos/100/100", width=100, height=100, border_radius=5, rotate=Rotate(angle=0.25 * pi, alignment=ft.Alignment.CENTER_LEFT) ) |
scale | Optional[ScaleValue] | None | 沿二维平面缩放此控件。默认缩放因子为 1.0,表示不缩放。 例如,将此属性设为 0.5 会让控件缩小一半,而设为 2.0 则会放大一倍。 通过将 Control.scale 属性设为 Scale 类的实例,可以为 x 和 y 轴指定不同的缩放倍数。可以指定 scale,或者指定 scale_x 和 scale_y,但不能同时指定全部。 示例: python ft.Image( src="https://picsum.photos/100/100", width=100, height=100, border_radius=5, scale=ft.Scale(scale_x=2, scale_y=0.5) ) |
offset | Optional[OffsetValue] | None | 在绘制控件之前应用平移变换。 平移量表示为按控件尺寸缩放的 Offset。例如,Offset(x=0.25, y=0) 会使控件沿水平方向平移其宽度的四分之一。 示例 下面的示例把容器显示在 Stack 的 0, 0 左上角,因为变换对控件应用了 -1 * 100, -1 * 100(即 offset * control's size)的水平与垂直平移: python import flet as ft def main(page: ft.Page): page.add( ft.Stack( width=1000, height=1000, controls=[ ft.Container( bgcolor=ft.Colors.RED, width=100, height=100, left=100, top=100, offset=ft.Offset(-1, -1), ) ], ) ) ft.run(main) |
flip | Optional[Flip] | None | 水平方向和/或垂直方向翻转此控件。 设为 Flip 的实例即可沿 x 轴、y 轴或两者镜像翻转。 |
transform | Optional[Transform] | None | 对此控件应用通用矩阵变换。 设为带 Matrix4 记录的 Transform 实例,用以描述任意的变换序列。 |
aspect_ratio | Optional[Number] | None | 控件的宽高比,定义为 width 与 height 的比值。 注意: 在当前实现中,如果设置了 aspect_ratio,同一控件上的 width 和 height 将在最终渲染尺寸中被忽略。 |
animate_opacity | Optional[AnimationValue] | None | 启用 opacity 属性的隐式动画。 更多信息见此处。 |
animate_size | Optional[AnimationValue] | None | 启用此控件尺寸的隐式动画。 如果设置了 width 或 height,则这些维度的变化会以动画呈现。如果两者都未设置,则对控件自然尺寸的变化进行动画。 更多信息见此处。 |
animate_position | Optional[AnimationValue] | None | 启用定位属性(left、right、top 和 bottom)的隐式动画。 更多信息见此处。 |
animate_align | Optional[AnimationValue] | None | 启用 align 属性的隐式动画。 更多信息见此处。 |
animate_margin | Optional[AnimationValue] | None | 启用 margin 属性的隐式动画。 更多信息见此处。 |
animate_rotation | Optional[AnimationValue] | None | 启用 rotate 属性的隐式动画。 更多信息见此处。 |
animate_scale | Optional[AnimationValue] | None | 启用 scale 属性的隐式动画。 更多信息见此处。 |
animate_offset | Optional[AnimationValue] | None | 启用 offset 属性的隐式动画。 更多信息见此处。 |
size_change_interval | int | 10 | on_size_change 事件的采样间隔(毫秒)。 设为 0 时,每次变化都会立即调用 on_size_change。 |
expand | Annotated[Optional[Union[bool, int]], V.instance_of((bool, int))] | None | 指定此控件是否/如何扩展以填满父布局中的可用空间。 更多信息见此处。 注意: 仅当此控件的直接父级是以下控件或其子类时才生效:Column、Row、View、Page。 抛出异常: ValueError:如果它不是 bool 或 int 类型。 |
expand_loose | bool | False | 允许控件在主轴方向有可用空间时扩展,但不要求它填满所有可用空间。 更多信息见此处。 注意: 如果 expand_loose 为 True,则仅在以下条件满足时才生效: - expand 不为 None,且 - 此控件的直接父级是以下控件或其子类之一:Column、Row、View、Page。 |
col | ResponsiveNumber | 12 | 如果此控件的父级是 ResponsiveRow,则此属性决定该控件在屏幕上跨越多少虚拟列。 可以是一个数字,也可以是一个按断点配置不同值的字典,例如 col={"sm": 6}。 值为 0 时会在该断点隐藏控件,因此它不会占用父级 ResponsiveRow 中的任何列。 此控件默认跨越 12 个虚拟列。 | 断点 | 尺寸 | |---|---| | xs | <576px | | sm | ≥576px | | md | ≥768px | | lg | ≥992px | | xl | ≥1200px | | xxl | ≥1400px | |
opacity | Annotated[Number, V.between(0.0, 1.0)] | 1.0 | 定义控件的透明度。 取值范围从 0.0(完全透明)到 1.0(完全不透明,无任何透明感)。 抛出异常: ValueError:如果它不在 0.0 到 1.0(含)之间。 |
tooltip | Optional[TooltipValue] | None | 鼠标悬停在此控件上时显示的工具提示。 |
badge | Optional[BadgeValue] | None | 显示在此控件顶部的徽章。 |
visible | bool | True | 每个控件都有 visible 属性,默认为 True —— 控件会渲染在页面上。将 visible 设为 False 会彻底阻止控件(及其所有子控件,如果有)渲染到页面画布上。隐藏的控件无法通过键盘或鼠标获得焦点或选中,也不会触发任何事件。 |
disabled | bool | False | 每个控件都有 disabled 属性,默认为 False —— 控件及其所有子控件都处于启用状态。 注意: 此属性的值会递归向下传播到所有子控件。 示例: 例如,如果你有一个包含多个输入控件的表单,可以通过禁用容器来一并禁用它们: python ft.Column( disabled = True, controls=[ ft.TextField(), ft.TextField() ] ) |
rtl | bool | False | 控件的文本方向是否为从右到左(RTL)。 |
data | Any | skip_ | 任意类型的自定义数据。 |
key | Optional['KeyValue'] | None | 用于在更新之间保持控件标识稳定的键。 |
只读属性
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
parent | Optional['BaseControl'] | — | 此控件的直接祖先(父级)。 默认为 None,只有在此控件被挂载(加入页面树)后才有值。 Page 控件(页面树的根)是个例外 —— 它的 parent 始终为 None。 |
page | 'Page' | — | 此控件所属的页面。 |
事件
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
on_load | Optional[ft.ControlEventHandler['Video']] | None | 当视频播放器初始化完成并可以播放时触发。 |
on_enter_fullscreen | Optional[ft.ControlEventHandler['Video']] | None | 当视频播放器进入全屏时触发。 |
on_exit_fullscreen | Optional[ft.ControlEventHandler['Video']] | None | 当视频播放器退出全屏时触发 |
on_error | Optional[ft.ControlEventHandler['Video']] | None | 当发生错误时触发。 事件处理器参数的 data 属性包含错误的相关信息。 |
on_complete | Optional[ft.ControlEventHandler['Video']] | None | 当视频播放器播放完成时触发。 |
on_track_change | Optional[ft.ControlEventHandler['Video']] | None | 当视频轨道变化时触发。 事件处理器参数的 data 属性包含新轨道的索引。 |
on_position_change | Optional[ft.ControlEventHandler['Video']] | None | 当前播放位置变化时触发。 事件处理器参数的 data 属性包含当前位置,类型为 Duration。 |
on_duration_change | Optional[ft.ControlEventHandler['Video']] | None | 当前媒体时长变化时触发。 事件处理器参数的 data 属性包含当前时长,类型为 Duration。 |
on_size_change | Optional[EventHandler[LayoutSizeChangeEvent['LayoutControl']]] | None | 当此控件的尺寸发生变化时调用。 size_change_interval 定义此事件的调用频率。 |
on_animation_end | Optional[ControlEventHandler['LayoutControl']] | None | 动画完成时调用。 可用于串联多个动画。 事件处理器参数的 data 属性包含动画名称。 更多信息见此处。 |
方法
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
before_update | — | — | — |
play | — | — | 开始播放视频。 |
pause | — | — | 暂停视频播放器。 |
play_or_pause | — | — | 在视频播放器的播放与暂停状态之间切换,即:暂停时播放,播放时暂停。 |
stop | — | — | 停止视频播放器。 |
next | — | — | 跳到 playlist 中的下一个 VideoMedia。 |
previous | — | — | 跳到 playlist 中的上一个 VideoMedia。 |
seek | — | — | 将 playlist 中当前播放的 VideoMedia 跳转到指定的 position。 |
jump_to | — | — | 跳到 playlist 中指定 media_index 处的 VideoMedia。 |
is_playing | bool | — | 返回: 如果视频播放器当前正在播放则返回 True,否则返回 False。 |
is_completed | bool | — | 返回: 如果视频播放器已到达当前播放媒体的末尾则返回 True,否则返回 False。 |
get_duration | ft.Duration | — | 返回: 当前播放媒体的时长。 |
get_current_position | ft.Duration | — | 返回: 当前播放媒体的当前位置。 |
take_screenshot | Optional[bytes] | — | 捕获当前视频帧的截图。 参数: format:要返回的图像格式。支持的值为 "image/png"(PNG 编码图像)、"image/jpeg"(JPEG 编码图像),以及 None(原生后端上的原始 BGRA 像素缓冲区)。 include_libass_subtitles:原生后端上是否在截图中包含 libass 字幕。这需要底层播放器支持 libass,在 Web 后端上会被忽略。 返回: 编码后的图像字节;如果当前后端无法捕获视频帧则返回 None。 抛出异常: ValueError:如果 format 不受支持。 |
is_isolated | — | — | 返回此控件是否被标记为隔离。 隔离控件会被排除在父级驱动的更新遍历之外,并需要自行管理更新边界。 |
init | — | — | 在控件实例初始化之后、首次 build / update 循环之前调用。 重写此钩子可在依赖已初始化字段时执行轻量级设置。不要在此处调用 update()。 |
build | — | — | 在控件初始化期间调用一次,用于定义其子控件。在此方法中 page 属性可用。 |
before_event | — | — | 在事件处理器执行之前拦截事件。 返回 False 可取消分发。返回 True 或 None 则继续正常的事件处理。 |
did_mount | — | — | 在控件挂载到页面树之后调用。 重写此方法以启动需要已挂载页面的资源,例如订阅、定时器或服务监听器。 |
will_unmount | — | — | 在控件从页面树移除之前调用。 重写此方法以释放 did_mount() 中创建的资源,例如订阅、定时器或外部句柄。 |
get_data_channel | — | — | 解析为此 widget 在 Dart 侧分配的 [DataChannel]。模式: on_data_channel_open: Optional[ft.EventHandler[DataChannelOpenEvent]] = None def init(self): self.on_data_channel_open = self._on_open def _on_open(self, e): self._channel = self.get_data_channel(e.channel_id) 幂等 —— 底层的 Connection 会按 id 缓存 DataChannel,因此重复调用会返回同一实例。没有错误路径:该 id 始终来自框架触发的事件,所以当此方法运行时,通道在两端都已存在。 |
update | None | — | 为此控件请求一次 UI 更新。 在更改控件状态或属性后调用。控件必须已挂载到页面且未被标记为冻结。 |