diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cab4df7e..08485c91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 44ccfaad..a6c2f92a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 69651ee1..3ef81d0f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/advanced-usage/custom-controls.md b/doc/advanced-usage/custom-controls.md index b45c9c23..67d84b73 100644 --- a/doc/advanced-usage/custom-controls.md +++ b/doc/advanced-usage/custom-controls.md @@ -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". diff --git a/doc/advanced-usage/offline.md b/doc/advanced-usage/offline.md index e3c7d3dc..252edb95 100644 --- a/doc/advanced-usage/offline.md +++ b/doc/advanced-usage/offline.md @@ -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 diff --git a/doc/contributing/todo.md b/doc/contributing/todo.md index 08c2b5ad..838e6d40 100644 --- a/doc/contributing/todo.md +++ b/doc/contributing/todo.md @@ -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 diff --git a/doc/index.md b/doc/index.md index b67a6fd1..c8e39381 100644 --- a/doc/index.md +++ b/doc/index.md @@ -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 + + + + ``` + + 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. diff --git a/doc/web.md b/doc/web.md new file mode 100644 index 00000000..934223c7 --- /dev/null +++ b/doc/web.md @@ -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. diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart deleted file mode 100644 index 2be05929..00000000 --- a/example/lib/generated_plugin_registrant.dart +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -// ignore_for_file: directives_ordering -// ignore_for_file: lines_longer_than_80_chars -// ignore_for_file: depend_on_referenced_packages - -import 'package:bccm_player/bccm_player_web.dart'; - -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -// ignore: public_member_api_docs -void registerPlugins(Registrar registrar) { - BccmPlayerWeb.registerWith(registrar); - registrar.registerMessageHandler(); -} diff --git a/example/web/index.html b/example/web/index.html index e39c4986..713bd429 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -1,5 +1,6 @@ +
- + - +