Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ jobs:

- run: make android-test

web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
with:
flutter-version: 3.47.2
channel: stable
cache: true

- run: flutter pub get

- run: flutter build web
working-directory: example

ios:
runs-on: macos-26
env:
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
## 2.2.0

- Feature: web support. The player is embedded as a platform view and plays back through
[`bccm-video-player`](https://www.npmjs.com/package/bccm-video-player), which the host
page must load as `window.bccmVideoPlayer` — see the Web docs.
- The web player draws its own controls and owns fullscreen, so `controlsConfig` and
custom control builders do not apply there.
- Downloads and Chromecast are unavailable on web and degrade rather than throwing
channel errors.
- Known limitation: the video is painted from the root `Overlay` so that it is never
re-parented, which means it is not clipped by ancestor scroll views.
- Feature: `BccmPlayerView.native`, a factory the docs already referenced but which was
never added. Uses the platform's own controls and fullscreen.
- Fix: the web implementation no longer uses `dart:html`, so `flutter build web --wasm`
works and the package has no analyzer suppressions left.

## 2.1.0

- Chore: `pigeon` 22 -> 28. Swift `@async` host APIs moved from completion handlers to
`async throws`; `ios/Classes/DownloaderApiImpl.swift` was updated to match.
- Feature: native test suites for iOS (`example/ios/RunnerTests`) and Android
(`android/src/test`), covering pigeon codec round-trips, enum ordinals, the downloader
state schema, and track/media-item mapping.
- Fix: `CastMediaItemConverter` wrote `KEY_ALBUM_TITLE` into `setArtist`, so a cast item
carrying both lost its artist.
- CI: separate `android`, `ios` and `web` jobs; nothing compiled the native code before.

## 2.0.0

- Breaking: requires Dart 3.13 / Flutter 3.47.
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,20 @@ This means the video is rendered in the native view hierarchy without any interm

- [x] iOS
- [x] Android
- [ ] ~~Web~~. Some groundwork is there, but it's not complete and it's not supported.
- [x] Web — with caveats, see below.

### Web

Web plays back through [`bccm-video-player`](https://www.npmjs.com/package/bccm-video-player),
which the host page has to load (see the "Web" section of the docs). Differences
from iOS and Android:

- **The player draws its own controls**, so `controlsConfig` and custom control
builders do not apply. Fullscreen is the player's, not the Flutter route.
- **No downloads and no Chromecast.** Neither has a web equivalent.
- **A single player in a page**, not feed-style playback. The video is not
clipped by ancestor scroll views, so layouts that crop it with
`ClipRect`/`OverflowBox` — short-form feeds in particular — do not work.

## Features

Expand Down
3 changes: 3 additions & 0 deletions doc/advanced-usage/custom-controls.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
### Custom controls

> **Not applicable on web.** The web player draws its own controls, so
> `controlsConfig` and `customBuilder` are ignored there. See [Web](../web.md).

#### Controls customization

For colors, etc, see "Styling".
Expand Down
2 changes: 2 additions & 0 deletions doc/advanced-usage/offline.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Offline

> **Not available on web.** See [Web](../web.md).

To support offlining, you need to add the DATA_SYNC permission to your AndroidManifest:

```xml
Expand Down
25 changes: 12 additions & 13 deletions doc/contributing/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,26 @@ Known gaps, roughly in order of value. Each is self-contained — none blocks th

Queue and audio work is tracked separately in [audio-support-plan.md](audio-support-plan.md); this list is everything outside that.

## Migrate the web player off `dart:html`

[`lib/src/web/video_js_player.dart`](../../lib/src/web/video_js_player.dart) is ~120 lines of DOM code on the deprecated `dart:html`. Moving to `package:web` + `dart:js_interop` needs a new dependency, `ui_web.platformViewRegistry` in place of `dart:ui`'s, and a replacement for `NodeTreeSanitizer.trusted`.

Only a real web build can verify it — no Dart test reaches this file. It carries the single inline `// ignore: deprecated_member_use` in the package, so this is the last thing between us and an unqualified strict `flutter analyze`.

## Make `tv_controls.dart` DVR-aware

[`lib/src/widgets/controls/tv/tv_controls.dart`](../../lib/src/widgets/controls/tv/tv_controls.dart) reimplements `useTimeline` inline and ignores `seekableRangeStartMs` / `seekableRangeEndMs` entirely, so seeking a live DVR window is wrong on TV.

Unlike the bug fixed in `default_controls`, it is at least self-consistent — its thumb and its drag agree with each other — so this is a missing feature rather than a mismatch. The fix is to make it call `useTimeline` and `positionFromFraction`, which also removes the duplication.

## Native tests

The six Robolectric tests in `android/src/test/` still aren't in CI. They need a JDK and the Android SDK, and run through the Flutter-generated Gradle project rather than from `android/` directly:
## Web

```sh
cd example/android && ./gradlew :bccm_player:testDebugUnitTest
```
The player is embedded as a platform view hosted in the root overlay
([`web_player_overlay.dart`](../../lib/src/widgets/video/web_player_overlay.dart)),
which is what stops the element being re-parented — re-parenting costs the media
element its source, so the video reloads. Remaining gaps:

iOS has no test target at all — `ios/bccm_player.podspec` has no `test_spec`.
- The video is not clipped by ancestor scroll views, so a player scrolled past
its viewport paints over whatever is beside it.
- `getPlayerTracks` reports `isSelected: false` for every track: the JS package
exposes track lists but not which one is current. Only matters if an app reads
tracks from Dart rather than using the player's own picker.
- Web uses the player's own skin rather than the Flutter controls, deliberately —
see the comment in `controlled_player_view.dart`.

## Minor

Expand Down
17 changes: 16 additions & 1 deletion doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,19 @@
...
```

4. For chromecast support, you need to do a few more things, check out the "Chromecast" docs.
4. (Web) Load the player bundle in `web/index.html`, before Flutter starts. The
plugin calls `window.bccmVideoPlayer`, and nothing plays without it:

```html
<link rel="stylesheet" href="https://unpkg.com/bccm-video-player@3.4.0/build/bccm-video-player.css">
<script src="https://unpkg.com/bccm-video-player@3.4.0/build/bccm-video-player.umd.js"></script>
<script>window.bccmVideoPlayer = window['btv-video'];</script>
```

The UMD build registers itself as `window['btv-video']`, hence the alias. A
real app should bundle [`bccm-video-player`](https://www.npmjs.com/package/bccm-video-player)
rather than load it from a CDN.

Read the "Web" page for what differs from iOS and Android.

5. For chromecast support, you need to do a few more things, check out the "Chromecast" docs.
74 changes: 74 additions & 0 deletions doc/web.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Web

Web support works, but it is not the same player as iOS and Android, and the
differences are worth knowing before you build against it.

## Setup

The host page must load [`bccm-video-player`](https://www.npmjs.com/package/bccm-video-player)
and expose it as `window.bccmVideoPlayer` — see step 4 of [Installation](index.md).
Without it, players are created but never play.

## The player draws its own controls

On iOS and Android the plugin draws Flutter controls over the video. On web the
underlying player draws its own, and **`BccmPlayerViewConfig.controlsConfig` —
including `customBuilder` — does not apply**.

This is deliberate. The video is a real DOM element, and it can only live in one
place at a time: drawing Flutter controls above it means hoisting them out of the
normal widget tree, which costs `Material` ancestry, layout and z-ordering. The
player's own skin is web-optimised and localised, so the trade is worth it.

Practically: `BccmPlayerView` and `BccmPlayerView.native` render the same thing
on web.

## Fullscreen

Fullscreen is the browser's, requested on the player element, rather than the
Flutter route used elsewhere. `BccmPlayerViewController.enterFullscreen()` still
works and still resolves when fullscreen ends, so calling code does not need to
change — but the fullscreen UI is the player's.

Pushing a Flutter route for it would rebuild the platform view and detach the
video from its source.

## Not available

- **Downloads.** No web equivalent, so `DownloaderInterface` reads empty and
`startDownload` throws `UnsupportedError`.
- **Chromecast.** The cast APIs are no-ops.
- **Picture in picture**, video textures, and `setMixWithOthers`.

## Known limitation: clipping

The video is painted from Flutter's root `Overlay` rather than inline, which is
what keeps it from being re-parented — re-parenting costs the media element its
source and the video reloads. The cost is that it is **not clipped by ancestor
scroll views**: a player scrolled past its viewport paints over whatever is
beside it.

If that matters more than reload-free scrolling for your layout, open an issue —
the trade is a small, contained change.

### What this rules out

Layouts that crop the video by clipping it. The common shape is an `OverflowBox`
sized past the frame inside a `ClipRect`, used to cover-crop a landscape stream
into a vertical one — a short-form/"shorts" feed being the usual case. On web the
clip does not apply, so the video paints at its overflowed size across the page.

The same layouts usually draw their own UI over the video, which the overlay also
paints above, and run several preloaded players at once, all of which would be
visible rather than hidden off-screen.

Feed-style playback is therefore not supported on web. A single player in a page
is what this is built for.

## Track selection

`getPlayerTracks` returns the available audio and subtitle tracks, and
`setSelectedTrack` switches them, but tracks are reported with
`isSelected: false` because the underlying API exposes the lists without saying
which is current. Use the player's own picker unless you are tracking selection
yourself.
17 changes: 0 additions & 17 deletions example/lib/generated_plugin_registrant.dart

This file was deleted.

38 changes: 15 additions & 23 deletions example/web/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>

<head>
<!--
If you are serving your web app in a path other than the root, change the
Expand All @@ -21,38 +22,29 @@
<meta name="description" content="Demonstrates how to use the bccm_player plugin.">

<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="bccm_player_example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">

<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<link rel="icon" type="image/png" href="favicon.png" />

<title>bccm_player_example</title>
<link rel="manifest" href="manifest.json">

<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
<!--
The plugin expects the host page to provide the bccm-video-player bundle as
window.bccmVideoPlayer. The UMD build registers itself as window['btv-video'],
so it is aliased below. A real app would bundle this rather than use a CDN.
-->
<link rel="stylesheet" href="https://unpkg.com/bccm-video-player@3.4.0/build/bccm-video-player.css">
<script src="https://unpkg.com/bccm-video-player@3.4.0/build/bccm-video-player.umd.js"></script>
<script>window.bccmVideoPlayer = window['btv-video'];</script>
</head>

<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
}
}).then(function(engineInitializer) {
return engineInitializer.initializeEngine();
}).then(function(appRunner) {
return appRunner.runApp();
});
});
</script>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

</html>
Loading
Loading