PythonIDE Docs
中文
简体中文

视频播放器 MiniApp

VideoPlayer、片源搜索、播放状态和播放设置。

演示 appui.VideoPlayerTabView:播放页、可搜索片源列表与播放设置表单。

#预期效果

运行后会出现视频播放器页面,片源搜索、播放区域和播放设置保持分区清晰。

#完整示例

python
import appui

DEFAULT_SOURCES = [
    {
        "id": "sintel",
        "title": "Sintel Trailer",
        "url": "https://media.w3.org/2010/05/sintel/trailer.mp4",
        "tag": "默认",
    },
    {
        "id": "bbb",
        "title": "Big Buck Bunny",
        "url": (
            "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/"
            "Big_Buck_Bunny_720_10s_1MB.mp4"
        ),
        "tag": "测试",
    },
]

state = appui.State(
    selected_id="sintel",
    query="",
    autoplay=False,
    loop=False,
    show_controls=True,
    status="正在播放 Sintel Trailer",
)


def selected_source():
    for item in DEFAULT_SOURCES:
        if item["id"] == state.selected_id:
            return item
    return DEFAULT_SOURCES[0]


def set_query(value):
    state.query = str(value)


def select_source(source_id):
    item = next((row for row in DEFAULT_SOURCES if row["id"] == source_id), DEFAULT_SOURCES[0])
    state.batch_update(selected_id=source_id, status=f"已切换到 {item['title']}")


def toggle_autoplay(value):
    state.batch_update(autoplay=bool(value), status="已更新自动播放设置")


def toggle_loop(value):
    state.batch_update(loop=bool(value), status="已更新循环播放设置")


def toggle_controls(value):
    state.batch_update(show_controls=bool(value), status="已更新控制条设置")


def source_row(item):
    def choose():
        select_source(item["id"])

    active = item["id"] == state.selected_id
    icon = "play.circle.fill" if active else "play.circle"
    return appui.Button(
        action=choose,
        content=appui.HStack(
            [
                appui.Image(system_name=icon).foreground_color("systemBlue"),
                appui.VStack(
                    [
                        appui.Text(item["title"]).font("headline"),
                        (
                            appui.Text(item["url"])
                            .font("caption")
                            .foreground_color("secondaryLabel")
                            .line_limit(1)
                        ),
                    ],
                    alignment="leading",
                    spacing=3,
                ),
                appui.Spacer(),
                appui.Text(item["tag"]).font("caption").foreground_color("secondaryLabel"),
            ],
            spacing=10,
        ),
    )


def filtered_sources():
    q = state.query.strip().lower()
    if not q:
        return DEFAULT_SOURCES
    return [
        item for item in DEFAULT_SOURCES
        if q in item["title"].lower() or q in item["url"].lower() or q in item["tag"].lower()
    ]


def source_key(item):
    return item["id"]


def source_list_rows():
    sources = filtered_sources()
    if not sources:
        return [
            appui.ContentUnavailableView(
                "没有匹配片源",
                system_image="magnifyingglass",
                description="换个关键词,或清空搜索条件。",
            )
        ]
    return [appui.ForEach(sources, row_builder=source_row, key=source_key)]


def player_page():
    item = selected_source()
    player = appui.VideoPlayer(
        url=item["url"],
        autoplay=state.autoplay,
        loop=state.loop,
        show_controls=state.show_controls,
        presentation="inline",
        allows_fullscreen=True,
        allows_pip=True,
        allows_airplay=True,
    ).frame(height=240)
    details = appui.Form(
        [
            appui.Section(
                [
                    appui.LabeledContent("标题", value=item["title"]),
                    appui.LabeledContent("标签", value=item["tag"]),
                    appui.LabeledContent("状态", value=state.status),
                    appui.Text(item["url"]).font("caption").foreground_color("secondaryLabel"),
                ],
                header="当前片源",
            ),
        ]
    )
    return appui.NavigationStack(
        appui.VStack([player, details], spacing=0)
            .navigation_title("播放器")
    )


def sources_page():
    rows = source_list_rows()
    return appui.NavigationStack(
        appui.List([
            appui.Section(
                rows,
                header="片源",
                footer="点击片源会立即更新播放页。",
            )
        ])
            .searchable(text=state.query, on_change=set_query)
            .navigation_title("片源")
    )


def settings_page():
    return appui.NavigationStack(
        appui.Form(
            [
                appui.Section(
                    [
                        appui.Toggle("自动播放", is_on=state.autoplay, on_change=toggle_autoplay),
                        appui.Toggle("循环播放", is_on=state.loop, on_change=toggle_loop),
                        appui.Toggle("显示控制条", is_on=state.show_controls, on_change=toggle_controls),
                    ],
                    header="播放设置",
                ),
            ]
        ).navigation_title("设置")
    )


def body():
    return appui.TabView(
        [
            appui.Tab("播放", system_image="play.rectangle", tag=0, content=player_page()),
            appui.Tab("片源", system_image="list.bullet", tag=1, content=sources_page()),
            appui.Tab("设置", system_image="gearshape", tag=2, content=settings_page()),
        ],
        selection=0,
    )


appui.run(body, state=state, presentation="fullscreen_with_close")

#关键技巧

  • VStackVideoPlayer 固定在页面上方,下方再用 Form/List,避免把播放器塞进普通列表段落。
  • 片源列表用 List(...).searchable(...),动态行通过 ForEach(..., key=...) 保持稳定身份;无匹配时展示空状态。
  • 播放器、地图、WebView 这类大媒体视图固定在页面主区域,下方再放 Form/List 控件。
  • VideoPlayer 可配 presentationallows_fullscreenallows_pipallows_airplay 等。