如何修复 flutter webview 中无法访问视频流(NotAllowedError)以使用 html5+webRTC 摄像头 api?

How to fix cannot access video stream (NotAllowedError) in flutter webview to use html5+webRTC camera api?

我正在使用 instascan.min js 库扫描我的网络 angular js 应用程序中的二维码。但是在我的 flutter web 视图中,我收到一个错误 - 无法访问视频流 (NotAllowedError),而且我无法修复它。我对flutter了解不多

我尝试授予相机访问权限,但没有成功。

//# 我的 Flutter 代码

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


void main () => runApp(MyApp());
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context) {
return MaterialApp(
  title: 'FOTOFACE WALLET',
  debugShowCheckedModeBanner: false,
  home: Home(),
);
}
}

class Home extends StatefulWidget{
@override
 _HomeState createState() => _HomeState();
}



class _HomeState extends State<Home>{
@override
Widget build(BuildContext context) {
 return WebviewScaffold(
  appBar: PreferredSize(
      preferredSize: Size.fromHeight(0),
      child: AppBar(
        automaticallyImplyLeading: true, // hides leading widget
        backgroundColor: new Color(0xFF404E67),
      )
  ),

  url: "https://fotofacewallet.com",
  initialChild: Center(
    child: CircularProgressIndicator(),
  ),
);
}
}

//# 这是我在 angular js controller

中的扫描代码
$scope.scan = () => {
var overlay = $('.overlay'),
close = $('<div class="close" id="closescanbtn">close</div>');
overlay.append(close);
let scanner = new Instascan.Scanner({
   video: document.getElementById('preview')
});

scanner.addListener('scan', function (content) {
scanner.stop();
$('.overlay').fadeOut();
$('.overlay').hide();
$scope.scanpayProcess(content);
});
Instascan.Camera.getCameras().then(function (cameras) {

if (cameras.length > 0) {
    if(cameras[1]){
        scanner.start(cameras[1]);
    } else {
        scanner.start(cameras[0]);
    } 
} else {
  alert('No cameras found.');
}


}).catch(function (e) {
alert(e);
});
$('.overlay').show();
}

我期待在 flutter web 视图中看到相机视图。

我做过这样的事

在我的 index.html 页面或您在 angular 端创建的文件

我添加了一个按钮:

<button type="button" onclick="displayMsg()" class="btn btn-default btn-login">Scan</button>

并如下处理其点击事件,我们将 post 消息作为 'scan'

<script type="text/javascript">
            function displayMsg(){
                Print.postMessage("scan");
            }
        </script>

在我的 pubspec.yaml 文件中添加了这个包

flutter_barcode_scanner: ^0.1.5+1

执行 运行 flutter pub get 更新依赖项

然后在我的 main.dart 文件上

我已经导入了

import 'dart:async';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';

在 main.dart 文件中添加了以下代码以处理在 displayMsg() 函数上触发的事件 - 'scan' 消息

  final Set<JavascriptChannel> jsChannels = [
      JavascriptChannel(
          name: 'Print',
          onMessageReceived: (JavascriptMessage message) {
            if(message.message == 'scan'){

              print(message.message);
              sBarcode(MyApp());
            }
          }),
    ].toSet(); 



  sBarcode(someVal) async {  
 String bCode = await FlutterBarcodeScanner.scanBarcode("#ff6666", "Cancel", true);   print(bCode);
   someVal.enterBarcode(bCode); // to get the scanned barcode    
return; }

enterBarcode(barc) {
    flutterWebViewPlugin.evalJavascript("document.getElementById('yourtextboxid').value="
+ barc);   }

这就是我完整的 main.dart 文件现在的样子

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';

const kAndroidUserAgent =
    'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Mobile Safari/537.36';


String selectedUrl = 'add_your_url_here';

// ignore: prefer_collection_literals
final Set<JavascriptChannel> jsChannels = [
  JavascriptChannel(
      name: 'Print',
      onMessageReceived: (JavascriptMessage message) {
        if(message.message == 'scan'){
          //MyApp.startBarcode();
          print(message.message);
          sBarcode(MyApp());
        }
      }),
].toSet();

sBarcode(someVal) async {
  String bCode = await FlutterBarcodeScanner.scanBarcode("#ff6666", "Cancel", true);
  print(bCode);
  someVal.enterBarcode(bCode);
  return;
}
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  final flutterWebViewPlugin = FlutterWebviewPlugin();

  enterBarcode(barc) {
    flutterWebViewPlugin.evalJavascript("document.getElementById('barcodenumber').value=" + barc);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter WebView Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
       // '/': (_) => const MyHomePage(title: 'Flutter WebView Demo'),
        '/': (_) {
          return WebviewScaffold(
            url: selectedUrl,
            javascriptChannels: jsChannels,
            withZoom: true,
            withLocalStorage: true,
            withJavascript: true,
            hidden: true,
            initialChild: Container(
              color: Colors.white,
              child: const Center(
                child: Text('Loading...'),
              ),
            ),
          );
        },
      },
    );
  }
}

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // Instance of WebView plugin
  final flutterWebViewPlugin = FlutterWebviewPlugin();

  // On destroy stream
  StreamSubscription _onDestroy;

  // On urlChanged stream
  StreamSubscription<String> _onUrlChanged;

  // On urlChanged stream
  StreamSubscription<WebViewStateChanged> _onStateChanged;

  StreamSubscription<WebViewHttpError> _onHttpError;

  StreamSubscription<double> _onProgressChanged;

  StreamSubscription<double> _onScrollYChanged;

  StreamSubscription<double> _onScrollXChanged;

  final _urlCtrl = TextEditingController(text: selectedUrl);

  final _codeCtrl = TextEditingController(text: 'window.navigator.userAgent');

  final _scaffoldKey = GlobalKey<ScaffoldState>();

  final _history = [];

  @override
  void initState() {
    super.initState();

    flutterWebViewPlugin.close();

    _urlCtrl.addListener(() {
      selectedUrl = _urlCtrl.text;
    });

    // Add a listener to on destroy WebView, so you can make came actions.
    _onDestroy = flutterWebViewPlugin.onDestroy.listen((_) {
      if (mounted) {
        // Actions like show a info toast.
        _scaffoldKey.currentState.showSnackBar(
            const SnackBar(content: const Text('Webview Destroyed')));
      }
    });

    // Add a listener to on url changed
    _onUrlChanged = flutterWebViewPlugin.onUrlChanged.listen((String url) {
      if (mounted) {
        setState(() {
          _history.add('onUrlChanged: $url');
        });
      }
    });

    _onProgressChanged =
        flutterWebViewPlugin.onProgressChanged.listen((double progress) {
      if (mounted) {
        setState(() {
          _history.add('onProgressChanged: $progress');
        });
      }
    });

    _onScrollYChanged =
        flutterWebViewPlugin.onScrollYChanged.listen((double y) {
      if (mounted) {
        setState(() {
          _history.add('Scroll in Y Direction: $y');
        });
      }
    });

    _onScrollXChanged =
        flutterWebViewPlugin.onScrollXChanged.listen((double x) {
      if (mounted) {
        setState(() {
          _history.add('Scroll in X Direction: $x');
        });
      }
    });

    _onStateChanged =
        flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) {
          print(state.type);
      if (mounted) {
        setState(() {
          _history.add('onStateChanged: ${state.type} ${state.url}');
        });
      }
    });

    _onHttpError =
        flutterWebViewPlugin.onHttpError.listen((WebViewHttpError error) {
      if (mounted) {
        setState(() {
          _history.add('onHttpError: ${error.code} ${error.url}');
        });
      }
    });
  }

  @override
  void dispose() {
    // Every listener should be canceled, the same should be done with this stream.
    _onDestroy.cancel();
    _onUrlChanged.cancel();
    _onStateChanged.cancel();
    _onHttpError.cancel();
    _onProgressChanged.cancel();
    _onScrollXChanged.cancel();
    _onScrollYChanged.cancel();

    flutterWebViewPlugin.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: const Text('Plugin example app'),
      ),
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              padding: const EdgeInsets.all(24.0),
              child: TextField(controller: _urlCtrl),
            ),
            RaisedButton(
              onPressed: () {
                flutterWebViewPlugin.launch(
                  selectedUrl,
                  rect: Rect.fromLTWH(
                      0.0, 0.0, MediaQuery.of(context).size.width, 300.0),
                  userAgent: kAndroidUserAgent,
                  invalidUrlRegex:
                      r'^(https).+(twitter)', // prevent redirecting to twitter when user click on its icon in flutter website
                );
              },
              child: const Text('Open Webview (rect)'),
            ),
            RaisedButton(
              onPressed: () {
                flutterWebViewPlugin.launch(selectedUrl, hidden: true);
              },
              child: const Text('Open "hidden" Webview'),
            ),
            RaisedButton(
              onPressed: () {
                flutterWebViewPlugin.launch(selectedUrl);
              },
              child: const Text('Open Fullscreen Webview'),
            ),
            RaisedButton(
              onPressed: () {
                Navigator.of(context).pushNamed('/widget');
              },
              child: const Text('Open widget webview'),
            ),
            Container(
              padding: const EdgeInsets.all(24.0),
              child: TextField(controller: _codeCtrl),
            ),
            RaisedButton(
              onPressed: () {
                final future =
                    flutterWebViewPlugin.evalJavascript(_codeCtrl.text);
                future.then((String result) {
                  setState(() {
                    _history.add('eval: $result');
                  });
                });
              },
              child: const Text('Eval some javascript'),
            ),
            RaisedButton(
              onPressed: () {
                setState(() {
                  _history.clear();
                });
                flutterWebViewPlugin.close();
              },
              child: const Text('Close'),
            ),
            RaisedButton(
              onPressed: () {
                flutterWebViewPlugin.getCookies().then((m) {
                  setState(() {
                    _history.add('cookies: $m');
                  });
                });
              },
              child: const Text('Cookies'),
            ),
            Text(_history.join('\n'))
          ],
        ),
      ),
    );
  }
}

检查并测试,让我知道这是否适合您,这在我这边工作正常。 打印会在浏览器中产生错误,您需要在 android 或 ios 中对此进行测试。 希望这会有所帮助。

您收到 Cannot access video stream (NotAllowedError) 错误,因为您需要授予 webview 正确的权限。

对于Android,在AndroidManifest.xml中,您需要添加这些权限:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.VIDEO_CAPTURE" />
<uses-permission android:name="android.permission.AUDIO_CAPTURE" />

但这还不够!要请求有关摄像头和麦克风的权限,您可以使用 permission_handler 插件。

因此,对于 webview,您可以使用我的插件 flutter_inappwebview and use the androidOnPermissionRequest event for Android, that is an event fired when the WebView is requesting permission to access the specified resources (that is the Android native WebChromeClient.onPermissionRequest event)。

在Android:

上使用WebRTC的例子
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:permission_handler/permission_handler.dart';

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Permission.camera.request();
  await Permission.microphone.request();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: InAppWebViewPage()
    );
  }
}

class InAppWebViewPage extends StatefulWidget {
  @override
  _InAppWebViewPageState createState() => new _InAppWebViewPageState();
}

class _InAppWebViewPageState extends State<InAppWebViewPage> {
  InAppWebViewController _webViewController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text("InAppWebView")
        ),
        body: Container(
            child: Column(children: <Widget>[
              Expanded(
                child: Container(
                  child: InAppWebView(
                      initialUrl: "https://appr.tc/r/158489234",
                      initialOptions: InAppWebViewGroupOptions(
                        crossPlatform: InAppWebViewOptions(
                          mediaPlaybackRequiresUserGesture: false,
                          debuggingEnabled: true,
                        ),
                      ),
                      onWebViewCreated: (InAppWebViewController controller) {
                        _webViewController = controller;
                      },
                      androidOnPermissionRequest: (InAppWebViewController controller, String origin, List<String> resources) async {
                        return PermissionRequestResponse(resources: resources, action: PermissionRequestResponseAction.GRANT);
                      }
                  ),
                ),
              ),
            ]))
    );
  }
}

此示例使用 https://appr.tc/, that is a video chat demo app based on WebRTC (https://github.com/webrtc/apprtc 上的 158489234 房间。 要让它工作,您需要将选项 mediaPlaybackRequiresUserGesture 设置为 false 并实施(对于 Android) onPermissionRequest 事件。