Skip to content

iOS VoiceOver disrupts the lifecycle of platform views #73716

@mehmetf

Description

@mehmetf

Internal: b/178003647

Reproduction with Youtube API + Webview Flutter plugin:

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title, style: TextStyle(fontFamily: 'ProductSans')),
      ),
      body: Center(
        child: TextButton(
          onPressed: () => _launchVideo(context), child: Text('Launch Video'),
        ),
      ),
    );
  }

  Future<void> _launchVideo(BuildContext context) async {
    await Navigator.push(
        context,
        MaterialPageRoute(
          fullscreenDialog: true,
          builder: (context) => _buildPopover(context),
        ));
  }

  Widget _buildPopover(BuildContext context) {
    return OrientationBuilder(
      builder: (context, orientation) {
        final isPortrait = orientation == Orientation.portrait;
        return Scaffold(
          appBar: isPortrait
              ? AppBar(
            title: Text('Video', ),
            leading: IconButton(
              icon: Icon(Icons.close),
              onPressed: () async {
                await Navigator.of(context).maybePop();
              },
            ),
          )
              : null,
          body: Container(
            decoration: BoxDecoration(
              color: Colors.black,
            ),
            child: SafeArea(child: YoutubePlayerWidget()),
          ),
        );
      },
    );
  }
}

const _userAgent =
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36';
const _aspectRatio = 16 / 9;

/// A youtube player widget which interacts with the underlying webview
/// to play YouTube videos.
class YoutubePlayerWidget extends StatefulWidget {
  @override
  _YoutubePlayerWidgetState createState() => _YoutubePlayerWidgetState();
}

class _YoutubePlayerWidgetState extends State<YoutubePlayerWidget> {
  @override
  Widget build(BuildContext context) {
    return AspectRatio(
        aspectRatio: _aspectRatio,
        child: IgnorePointer(
          ignoring: false,
          child: WebView(
            key: widget.key,
            userAgent: _userAgent,
            initialUrl: _player,
            javascriptMode: JavascriptMode.unrestricted,
            initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow,
          ),
        ));
  }

  String get _player {
    final _player = '''
    <!DOCTYPE html>
    <html>
    <head>
        <style>
            html,
            body {
                margin: 0;
                padding: 0;
                background-color: #000000;
                overflow: hidden;
                position: fixed;
                height: 100%;
                width: 100%;
            }
        </style>
        <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>
    </head>
    <body>
        <div id="player"></div>
        <script>
            var tag = document.createElement('script');
            tag.src = "https://www.youtube.com/iframe_api";
            var firstScriptTag = document.getElementsByTagName('script')[0];
            firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
            var player;
            var timerId;
            function onYouTubeIframeAPIReady() {
                player = new YT.Player('player', {
                    height: '100%',
                    width: '100%',
                    videoId: '6wnPeqh4m2k',
                    host: 'https://www.youtube.com',
                    playerVars: {
                        'controls': 1,
                        'playsinline': 1,
                        'enablejsapi': 1,
                        'fs': 0,
                        'rel': 0,
                        'showinfo': 0,
                        'iv_load_policy': 3,
                        'modestbranding': 1,
                        'cc_load_policy': 0,
                        'cc_lang_pref': 'en',
                        'autoplay': true
                    },
                    events: {
                        onError: (error) => Errors.postMessage(error.data)
                    },
                });
            }
        </script>
    </body>
    </html>
    ''';
    return 'data:text/html;base64,'
        '${base64Encode(const Utf8Encoder().convert(_player))}';
  }
}

Steps:

  1. Tap launch video.
  2. After the video starts playing, enable picture-in-picture mode by tapping on this icon:

image

  1. Tap close button.

Observe that the video stops and the platform view is gone.

Now enable voiceover and repeat the same steps. You will see that after tapping close, the video continues to play. The dispose logic of platform view is not executed correctly with voiceover on.

Metadata

Metadata

Assignees

Labels

P0Critical issues such as a build break or regressiona: accessibilityAccessibility, e.g. VoiceOver or TalkBack. (aka a11y)a: platform-viewsEmbedding Android/iOS views in Flutter appscustomer: mulligan (g3)found in release: 1.22Found to occur in 1.22has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: webviewThe WebView pluginpackageflutter/packages repository. See also p: labels.platform-iosiOS applications specificallyr: fixedIssue is closed as already fixed in a newer version

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions