-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
What package does this bug report belong to?
video_player_android (2.8.1) - it can't be reproduced in the main package (video_player) yet, since the platform view feature is not yet exposed by it.
What target platforms are you seeing this bug on?
Android (seems to happen below API 34, but I'm not entirely sure about this).
Steps to reproduce
- Replace the example app's code with the one attached below (it adds a tab that shows a video in a list).
- Run the example app.
- Tap on "List example" tab, then on "Platform view" tab below.
- Scroll until you can see the video.
- Scroll down, so that the video hides under
TabBarwidgets.
Expected results
Player is not visible anymore since it was scroll out of the visible viewport.
Actual results
Player shows on top of Flutter's UI. Just the player itself, without any transformations made to it, and without any widgets placed on top of it in Flutter (play/pause button, progress bar).
Screenshots or Videos
Video demonstration
Screen.Recording.2025-03-10.at.10.47.37.mov
Additional information
I also did some other tests with the platform view and looks like the issue happens when the video player gets out of visible view, but the dispose method was not yet called on the widget embedding the player. When dispose gets called, the floating player disappears.
The issue does not seem to happen on all devices. I observed it on most of the emulators and devices below API 34, but could not see it on emulators with API 34 and API 35 (although I'm not sure if this is a reliable information).
The problematic code seems to be the SurfaceView that is used by video_player_android to render a platform view. Seems like, as long as it's visible, the Flutter can properly handle it, but as soon as it disappears from the visible viewport, it punches a hole in Flutter's UI until it gets disposed (though again, it doesn't happen for all devices).
Code sample
Example app code with a list
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:video_player_platform_interface/video_player_platform_interface.dart';
import 'mini_controller.dart';
void main() {
runApp(
MaterialApp(
home: _App(),
),
);
}
class _App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
key: const ValueKey<String>('home_page'),
appBar: AppBar(
title: const Text('Video player example'),
bottom: const TabBar(
isScrollable: true,
tabs: <Widget>[
Tab(icon: Icon(Icons.cloud), text: 'Remote'),
Tab(icon: Icon(Icons.videocam), text: 'RTSP'),
Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
Tab(icon: Icon(Icons.list), text: 'List example'),
],
),
),
body: TabBarView(
children: <Widget>[
_ViewTypeTabBar(
builder: (VideoViewType viewType) =>
_BumbleBeeRemoteVideo(viewType),
),
_ViewTypeTabBar(
builder: (VideoViewType viewType) => _RtspRemoteVideo(viewType),
),
_ViewTypeTabBar(
builder: (VideoViewType viewType) =>
_ButterFlyAssetVideo(viewType),
),
_ViewTypeTabBar(
builder: (VideoViewType viewType) =>
_ButterFlyAssetVideoInList(viewType),
),
],
),
),
);
}
}
class _ViewTypeTabBar extends StatefulWidget {
const _ViewTypeTabBar({
required this.builder,
});
final Widget Function(VideoViewType) builder;
@override
State<_ViewTypeTabBar> createState() => _ViewTypeTabBarState();
}
class _ViewTypeTabBarState extends State<_ViewTypeTabBar>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TabBar(
controller: _tabController,
isScrollable: true,
tabs: const <Widget>[
Tab(
icon: Icon(Icons.texture),
text: 'Texture view',
),
Tab(
icon: Icon(Icons.construction),
text: 'Platform view',
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
widget.builder(VideoViewType.textureView),
widget.builder(VideoViewType.platformView),
],
),
),
],
);
}
}
class _ButterFlyAssetVideo extends StatefulWidget {
const _ButterFlyAssetVideo(this.viewType);
final VideoViewType viewType;
@override
_ButterFlyAssetVideoState createState() => _ButterFlyAssetVideoState();
}
class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> {
late MiniController _controller;
@override
void initState() {
super.initState();
_controller = MiniController.asset(
'assets/Butterfly-209.mp4',
viewType: widget.viewType,
);
_controller.addListener(() {
setState(() {});
});
_controller.initialize().then((_) => _controller.play());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
padding: const EdgeInsets.only(top: 20.0),
),
const Text('With assets mp4'),
Container(
padding: const EdgeInsets.all(20),
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller),
],
),
),
),
],
),
);
}
}
class _BumbleBeeRemoteVideo extends StatefulWidget {
const _BumbleBeeRemoteVideo(this.viewType);
final VideoViewType viewType;
@override
_BumbleBeeRemoteVideoState createState() => _BumbleBeeRemoteVideoState();
}
class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
late MiniController _controller;
@override
void initState() {
super.initState();
_controller = MiniController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
viewType: widget.viewType,
);
_controller.addListener(() {
setState(() {});
});
_controller.initialize();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
Container(padding: const EdgeInsets.only(top: 20.0)),
const Text('With remote mp4'),
Container(
padding: const EdgeInsets.all(20),
child: AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
VideoPlayer(_controller),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller),
],
),
),
),
],
),
);
}
}
class _RtspRemoteVideo extends StatefulWidget {
const _RtspRemoteVideo(this.viewType);
final VideoViewType viewType;
@override
_RtspRemoteVideoState createState() => _RtspRemoteVideoState();
}
class _RtspRemoteVideoState extends State<_RtspRemoteVideo> {
MiniController? _controller;
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
Future<void> changeUrl(String url) async {
if (_controller != null) {
await _controller!.dispose();
}
setState(() {
_controller = MiniController.network(
url,
viewType: widget.viewType,
);
});
_controller!.addListener(() {
setState(() {});
});
return _controller!.initialize();
}
String? _validateRtspUrl(String? value) {
if (value == null || !value.startsWith('rtsp://')) {
return 'Enter a valid RTSP URL';
}
return null;
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
Container(padding: const EdgeInsets.only(top: 20.0)),
const Text('With RTSP streaming'),
Padding(
padding: const EdgeInsets.all(20.0),
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: const InputDecoration(label: Text('RTSP URL')),
validator: _validateRtspUrl,
textInputAction: TextInputAction.done,
onFieldSubmitted: (String value) {
if (_validateRtspUrl(value) == null) {
changeUrl(value);
} else {
setState(() {
_controller?.dispose();
_controller = null;
});
}
},
),
),
if (_controller != null)
Container(
padding: const EdgeInsets.all(20),
child: AspectRatio(
aspectRatio: _controller!.value.aspectRatio,
child: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
VideoPlayer(_controller!),
_ControlsOverlay(controller: _controller!),
VideoProgressIndicator(_controller!),
],
),
),
),
],
),
);
}
}
class _ButterFlyAssetVideoInList extends StatelessWidget {
const _ButterFlyAssetVideoInList(this.viewType);
final VideoViewType viewType;
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
const _ExampleCard(title: 'Item a'),
const _ExampleCard(title: 'Item b'),
const _ExampleCard(title: 'Item c'),
const _ExampleCard(title: 'Item d'),
const _ExampleCard(title: 'Item e'),
const _ExampleCard(title: 'Item f'),
const _ExampleCard(title: 'Item g'),
Card(
child: Column(
children: <Widget>[
const ListTile(
leading: Icon(Icons.cake),
title: Text('Video video'),
),
_ButterFlyAssetVideo(viewType),
],
),
),
const _ExampleCard(title: 'Item h'),
const _ExampleCard(title: 'Item i'),
const _ExampleCard(title: 'Item j'),
const _ExampleCard(title: 'Item k'),
const _ExampleCard(title: 'Item l'),
const _ExampleCard(title: 'Item m'),
const _ExampleCard(title: 'Item n'),
const _ExampleCard(title: 'Item o'),
],
);
}
}
class ViewType {}
class _ControlsOverlay extends StatelessWidget {
const _ControlsOverlay({required this.controller});
static const List<double> _examplePlaybackRates = <double>[
0.25,
0.5,
1.0,
1.5,
2.0,
3.0,
5.0,
10.0,
];
final MiniController controller;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 50),
reverseDuration: const Duration(milliseconds: 200),
child: controller.value.isPlaying
? const SizedBox.shrink()
: const ColoredBox(
color: Colors.black26,
child: Center(
child: Icon(
key: ValueKey<String>('Play'),
Icons.play_arrow,
color: Colors.white,
size: 100.0,
semanticLabel: 'Play',
),
),
),
),
GestureDetector(
onTap: () {
controller.value.isPlaying ? controller.pause() : controller.play();
},
),
Align(
alignment: Alignment.topRight,
child: PopupMenuButton<double>(
initialValue: controller.value.playbackSpeed,
tooltip: 'Playback speed',
onSelected: (double speed) {
controller.setPlaybackSpeed(speed);
},
itemBuilder: (BuildContext context) {
return <PopupMenuItem<double>>[
for (final double speed in _examplePlaybackRates)
PopupMenuItem<double>(
value: speed,
child: Text('${speed}x'),
)
];
},
child: Padding(
padding: const EdgeInsets.symmetric(
// Using less vertical padding as the text is also longer
// horizontally, so it feels like it would need more spacing
// horizontally (matching the aspect ratio of the video).
vertical: 12,
horizontal: 16,
),
child: Text('${controller.value.playbackSpeed}x'),
),
),
),
],
);
}
}
/// A filler card to show the video in a list of scrolling contents.
class _ExampleCard extends StatelessWidget {
const _ExampleCard({required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const Icon(Icons.airline_seat_flat_angled),
title: Text(title),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: OverflowBar(
alignment: MainAxisAlignment.end,
spacing: 8.0,
children: <Widget>[
TextButton(
child: const Text('BUY TICKETS'),
onPressed: () {
/* ... */
},
),
TextButton(
child: const Text('SELL TICKETS'),
onPressed: () {
/* ... */
},
),
],
),
),
],
),
);
}
}Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.27.3, on macOS 14.4.1 23E224 darwin-arm64, locale en-PL)
• Flutter version 3.27.3 on channel stable at /Users/paweljakubowski/Development/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision c519ee916e (7 weeks ago), 2025-01-21 10:32:23 -0800
• Engine revision e672b006cb
• Dart version 3.6.1
• DevTools version 2.40.2
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
• Android SDK at /Users/paweljakubowski/Library/Android/sdk
• Platform android-35, build-tools 35.0.0
• ANDROID_HOME = /Users/paweljakubowski/Library/Android/sdk
• Java binary at: /opt/homebrew/opt/openjdk@17/bin/java
• Java version OpenJDK Runtime Environment Homebrew (build 17.0.13+0)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 15F31d
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 2024.2)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 21.0.4+-12422083-b607.1)
[✓] VS Code (version 1.98.0)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.106.0
[✓] Connected device (4 available)
• Android SDK built for arm64 (mobile) • emulator-5556 • android-arm64 • Android 8.1.0 (API 27) (emulator)
• macOS (desktop) • macos • darwin-arm64 • macOS 14.4.1 23E224 darwin-arm64
• Mac Designed for iPad (desktop) • mac-designed-for-ipad • darwin • macOS 14.4.1 23E224 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 133.0.6943.142
[✓] Network resources
• All expected network resources are available.