---
url: https://nsh.dev/zh-TW/elements/video
description: "Video 元素:使用原生影片播放,可搭配自訂或系統內建的控制項。"
demo: https://nsh.dev/play/index.html#el-video
---

# Video

video view 會使用平台的播放器來播放影片檔案或串流:在 iOS 和 Mac Catalyst 上使用 AVPlayer,在 Android 上使用 MediaPlayer。播放是透過 props(`paused`、`muted`、`rate` 等)來控制,因此播放器會像其他 view 一樣跟隨你的狀態。Rust 使用 `video(url)` 建立,TSX 則使用來自 `nsh` 的 `<Video>`。

```rust
use nsh_core::prelude::*;

fn player() -> impl IntoElement {
    let paused = signal(true);
    let time = signal(String::new());
    let player = element_ref();
    view()
        .gap_3()
        .child(
            video("https://example.com/clip.mp4")
                .element_ref(&player)
                .paused(paused)
                .repeat(true)
                .object_fit(ResizeMode::Contain)
                .h(px(220.0))
                .bg(black())
                .on_progress(move |p: &VideoProgress| {
                    time.set(format!("{:.1} / {:.1} s", p.current_time, p.duration))
                }),
        )
        .child(
            view()
                .flex_row()
                .gap_4()
                .child(
                    text(move || if paused.get() { "Play" } else { "Pause" })
                        .on_press(move || paused.update(|p| *p = !*p)),
                )
                .child(text("Restart").on_press(move || player.seek(0.0)))
                .child(text(move || time.get())),
        )
}
```

```tsx
import { signal, Text, Video, type VideoRef, View } from "nsh";

function Player() {
  const paused = signal(true);
  const time = signal("");
  let video!: VideoRef;
  return (
    <View style={{ gap: 12 }}>
      <Video
        ref={video}
        src="https://example.com/clip.mp4"
        paused={paused()}
        repeat
        style={{ height: 220, backgroundColor: "#000000" }}
        onProgress={(e) => time.set(`${e.currentTime.toFixed(1)} / ${e.duration.toFixed(1)} s`)}
      />
      <View style={{ flexDirection: "row", gap: 16 }}>
        <Text onPress={() => paused.set(!paused())}>{paused() ? "Play" : "Pause"}</Text>
        <Text onPress={() => video.seek(0)}>Restart</Text>
        <Text>{time()}</Text>
      </View>
    </View>
  );
}
```

在 TSX 中,`src` 為必填(請參閱 [Video API](https://nsh.dev/translations/zh-TW/modules/nsh-video.md))。請參閱 [web view and video guide](https://nsh.dev/translations/zh-TW/docs/web-view-and-video.md)。

## Props

video 具備 [view](https://nsh.dev/translations/zh-TW/elements/view.md) 的 props(它沒有子元素),另外還有:

| Prop | Rust | 型別 | 預設值 | 說明 |
|---|---|---|---|---|
| `src` | `video(url)` | `string` | | 影片的 URL(在 Rust 中也可使用資源的 `uri`:`video(CLIP.uri)`)。 |
| `paused` | `.paused(..)` | `boolean` | `false` | 播放已暫停。 |
| `muted` | `.muted(..)` | `boolean` | `false` | 靜音。 |
| `volume` | `.volume(..)` | `number` | `1` | 0..1。 |
| `rate` | `.rate(..)` | `number` | `1` | 播放速度。 |
| `repeat` | `.repeat(..)` | `boolean` | `false` | 播放到結尾時會從頭重新播放。 |
| `controls` | `.controls(..)` | `boolean` | `false` | 顯示平台的播放控制項。 |
| `objectFit`(style)、`object-*` classes | `.object_fit(ResizeMode::Contain)` | `"cover" \| "contain" \| "fill" \| "none" \| "scale-down"` | `"contain"` | 影片如何填入其容器,與 CSS 的 `object-fit` 相同(`class="object-cover"`)。iOS、macOS 和 Android 會將 `"none"` 和 `"scale-down"` 視為 `"contain"`播放,Linux 則將兩者都視為 `"scale-down"`。 |
| `progressUpdateInterval` | `.progress_interval(ms)` | `number` | `250` | `onProgress` 事件之間相隔的毫秒數。 |

請透過樣式為影片指定大小;影片的大小就是其樣式所給定的大小。若想使用影片本身的比例,請依據 `onLoad` 回報的尺寸來設定長寬比。

## Events

| 事件 | Rust | Payload | 觸發時機 |
|---|---|---|---|
| `onLoadStart` | `.on_load_start` | 無 | 開始載入。 |
| `onLoad` | `.on_load` | `{ duration, size }`(`VideoInfo`:`duration`、`size`) | 影片已就緒:提供以秒為單位的長度與以像素為單位的尺寸。 |
| `onProgress` | `.on_progress` | `{ currentTime, duration }`(`VideoProgress`:`current_time`、`duration`) | 播放期間每隔 `progressUpdateInterval` 觸發一次(單位為秒)。 |
| `onBuffer` | `.on_buffer` | `{ isBuffering }`(`bool`) | 播放因等待資料而停滯/已恢復。 |
| `onEnd` | `.on_end` | 無 | 播放已到達結尾(搭配 `repeat` 時,在每次循環結束時觸發)。 |
| `onError` | `.on_error` | `{ message, code? }`(`&str`,即訊息) | 載入或播放失敗;若來源未被應用程式的 capabilities 授權,`code` 為 `"ERR_ACCESS_DENIED"`。 |

```rust
let ratio = signal(16.0_f32 / 9.0);

video(url)
    .w_full()
    .aspect_ratio(ratio)
    .on_load(move |info: &VideoInfo| {
        ratio.set(info.size.width.0 as f32 / info.size.height.0 as f32)
    })
```

```tsx
const ratio = signal(16 / 9);
<Video src={url} style={{ width: "100%", aspectRatio: ratio() }} onLoad={(e) => ratio.set(e.size.width / e.size.height)} />;
```

## Ref methods

| 方法 | Rust | 說明 |
|---|---|---|
| `seek(seconds)` | `seek(seconds)` | 跳轉到指定時間。 |

## Capabilities

當應用程式宣告 [capabilities](https://nsh.dev/translations/zh-TW/docs/sandbox-and-capabilities.md) 時,影片來源只會從 `network` 允許的主機載入;來自其他主機的來源會顯示空白的 view,並以 `code` `"ERR_ACCESS_DENIED"` 回報 `onError`。

<a id="platform-notes"></a>

## 平台備註

- iOS 會在 `AVPlayerLayer` 中繪製影片,若使用 `controls`,則使用系統播放器 view;AVFoundation 會在第一次建立影片時載入。
- Android 在 TextureView 上使用 MediaPlayer 播放,並以 MediaController 提供 `controls`。
