Repository navigation
[video_player_android] Video gets on top of the UI when using platform view and the player is scrolled out of view #164899
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.
Activity
stuartmorgan-g commentedon Mar 10, 2025
Is the
SurfaceViewpresent in the view hierarchy when the platform view is first created? From skimming the code it looks like it probably isn't, and there's a known issue with TLHC where the logic that's supposed to bypass TLHC for plugins withSurfaceViews (since they don't work in TLHC) won't trigger in that case.If you add a dummy
SurfaceViewto the view returned byPlatformVideoViewFactory, does it fix the issue?FirentisTFW commentedon Mar 10, 2025
Thanks for the tip. I think the
SurfaceViewshould be there when creating a platform view, it is returned byPlatformVideoView.Yes, it works with a dummy
SurfaceView. It looks like attaching theSurfaceViewto theExoPlayeris the problematic part, even if we try to de-attach it later, one frame seems to stick there and cause the issue. I think I might find a workaround with observing the player and hiding/showing it on the native side, based on whether it is visible or not, but I'm not sure whether it'd be nice (and if this will work in the first place). I probably need to somehow get information about the UI that is rendered by Flutter and could obscure part of the view.Or do you have a different idea on how this issue could be solved? Or why does it happen in the first place when using
ExoPlayerand whether this is something that Flutter's rendering should handle?stuartmorgan-g commentedon Mar 10, 2025
It looks like I misread the code this morning, sorry for the bad pointer there. It's interesting though; having the
SurfaceViewshowing in above all content at a fixed location is the same behavior we would see with failure to fall back from TLHC, so maybe there's still something related happening?I think this is going to require someone to dig into some platform view-side debugging in the engine to see if anything jumps out there, and also to see if HC++ resolves this. Adding
team-androidfor direction to the right person.[video_player_android] Add note about Android platform views known is…
[video_player_android] Add note about Android platform views known is…
[video_player_android] Add note about Android platform views known is…
1 remaining item
ihijazi commentedon Jul 20, 2025
Also can confirm this is happening on:
Pixel 9, Android v15
Pixel 9 Pro XL, Android v15
Pixel 9, Android v16
Doesn't happen on Pixel 6 Pro, Android v13
Hopefully this can get better priority. In a list with different videos, this is basically not usable.
flutter-triage-bot commentedon Jul 22, 2025
This issue is assigned to @gmackall but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!
gmackall commentedon Aug 28, 2025
I was unable to repro this on a pixel 9 fold and a pixel 9 pro. Will need a bit to get my hands on a regular pixel 9 or a pixel 9 pro xl
gmackall commentedon Sep 16, 2025
I'm not able to repro this on a pixel 9 w/ android 16 either, using the sample code on flutter 3.32.5:
no_repro_video_on_top.mp4
I've tried debug and release builds and am not able to encounter the problem
flutter-triage-bot commentedon Jan 21, 2026
This issue is assigned to @gmackall but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!
flutter-triage-bot commentedon Apr 8, 2026
This issue was assigned to @gmackall but has had no status updates in a long time. To remove any ambiguity about whether the issue is being worked on, the assignee was removed.
gmackall commentedon Apr 8, 2026
I believe this likely has the same root cause as #175546 did, which is that clipping of surface views doesn't work with the apis we were using.
I was not able to reproduce this issue, but I would be curious if anyone hitting this finds it fixed when testing on a device on android api 34+, and passing the
--enable-hcppflag to flutter run.[video_player_android] Add note about Android platform views known is…
[video_player_android] Add note about Android platform views known is…