Search code examples
flutterdarthttpsafarihttpserver

Dart : HttpServer not responding correctly to .mp4 files on iOS Webview and iOS browsers


import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Appname',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Sunrion'),
    );
  }
}

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

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Future<String> initialize() async {
    await _getServer();

    if (kDebugMode) {
      debugPrint('appDocumentsDirectory : ${appDocumentsDirectory.path}');
    }

    final preferences = await SharedPreferences.getInstance();

    final isExtracted = preferences.getBool('isExtracted');

    if (isExtracted != true) {
      const platform = MethodChannel('flutter.native/zipchannel');
      try {
        var pathToAssetsZip = 'assets/resources/app.zip';

        await platform.invokeMethod('extractAssetsZip', {
          'name': pathToAssetsZip,
          'path': '${appDocumentsDirectory.path}/'
        });

        preferences.setBool('isExtracted', true);
      } catch (_) {}
    }

    return appDocumentsDirectory.path;
  }

  HttpServer? server;

  startServer() async {
    await for (var request in server!) {
      final path = request.uri.path;

      var filePath = '${appDocumentsDirectory.path}/app$path';

      final file = File(filePath);

      var existsString = '';

      if (path.endsWith('.mp4')) {
        final data = await file.readAsBytes();

        request.response
          ..headers.contentType = ContentType('video', 'mp4')
          ..add(data)
          ..close();

      } else if (path.endsWith('.png')) {
        final exists = file.existsSync();

        existsString = ' :: $exists';

        request.response
          ..add(await file.readAsBytes())
          ..close();
      } else if (path.endsWith('.js') || path.endsWith('.css')) {
        final fileContent = await file.readAsString();

        final exists = file.existsSync();

        existsString = ' :: $exists';

        final mime = path.endsWith('.js') ? 'javascript' : 'css';

        request.response
          ..headers.contentType = ContentType('text', mime, charset: 'utf-8')
          ..write(fileContent)
          ..close();
      } else if (path == '/') {
        filePath = '${appDocumentsDirectory.path}/app/index.html';

        final file = File(filePath);

        final fileContent = await file.readAsString();

        request.response
          ..headers.contentType = ContentType("text", "html", charset: "utf-8")
          ..write(fileContent)
          ..close();
      } else {
        request.response
          ..headers.contentType = ContentType("text", "plain", charset: "utf-8")
          ..write('Unknown path')
          ..close();
      }

      debugPrint('requestpath : $path$existsString');
    }
  }

  late Directory appDocumentsDirectory;

  Future<HttpServer> _getServer() async {
    if (server == null) {
      appDocumentsDirectory = await getApplicationDocumentsDirectory();

      server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);

      startServer();
    }

    if (kDebugMode) {
      print(
          "Server running on IP : ${server!.address} On Port : ${server!.port}");
    }
    return server!;
  }

  late WebViewController webViewController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder(
          future: initialize(),
          builder: (context, snapshot) {
            if (snapshot.connectionState != ConnectionState.done) {
              return Container();
            }

            final address = server!.address.address;

            var htmlPath = 'http://$address:${server!.port}';


            return Column(
              children: [
                Expanded(
                  child: WebView(
                    backgroundColor: Colors.white,
                    javascriptMode: JavascriptMode.unrestricted,
                    onWebViewCreated: (controller) async {
                      webViewController = controller;
                      await controller.loadUrl(htmlPath);
                    },
                    initialMediaPlaybackPolicy:
                        AutoMediaPlaybackPolicy.always_allow,
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(8),
                  child: TextButton(
                    onPressed: () {
                      webViewController.loadUrl(htmlPath);
                    },
                    child: const Text('Hit me'),
                  ),
                )
              ],
            );
          }),
    );
  }
}

enter image description here

I am trying to load the SPA in WebView made from reactjs which is placed in apps DocumentsDirectory.

The webview is loading HTML page but not the video file in it. There is a byte range concept which I have tried to implement in the HTTPserver but failed to implement.

I might be making a mistake in the mp4 IF condition above. A help would really appreciated.


Solution

  • I have used the shelf library (https://pub.dev/packages/shelf) which helps to manage the response for chunked data.

    import 'dart:convert';
    import 'dart:io';
    
    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    import 'package:shelf/shelf.dart';
    import 'package:webview_flutter/webview_flutter.dart';
    import 'package:path_provider/path_provider.dart';
    import 'package:shelf/shelf_io.dart' as shelf_io;
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Appname',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Sunrion'),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key, required this.title});
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      Future<String> initialize() async {
        await _getServer();
    
        if (kDebugMode) {
          debugPrint('appDocumentsDirectory : ${appDocumentsDirectory.path}');
        }
    
        final preferences = await SharedPreferences.getInstance();
    
        final isExtracted = preferences.getBool('isExtracted');
    
        if (isExtracted != true) {
          const platform = MethodChannel('flutter.native/zipchannel');
          try {
            var pathToAssetsZip = 'assets/resources/app.zip';
    
            await platform.invokeMethod('extractAssetsZip', {
              'name': pathToAssetsZip,
              'path': '${appDocumentsDirectory.path}/'
            });
    
            preferences.setBool('isExtracted', true);
          } catch (_) {}
        }
    
        return appDocumentsDirectory.path;
      }
    
      HttpServer? server;
    
      late Directory appDocumentsDirectory;
    
      Future<HttpServer> _getServer() async {
        if (server == null) {
          appDocumentsDirectory = await getApplicationDocumentsDirectory();
    
          var handler = const Pipeline()
              .addMiddleware(logRequests())
              .addHandler(_echoRequest);
    
          server = await shelf_io.serve(handler, InternetAddress.loopbackIPv4, 8080);
    
          server!.autoCompress = true;
        }
    
        if (kDebugMode) {
          print(
              "Server running on IP : ${server!.address} On Port : ${server!.port}");
        }
        return server!;
      }
    
      Future<Response> _echoRequest(Request request) async {
        final path = request.url.path;
    
        var filePath = '${appDocumentsDirectory.path}/app/$path';
    
        // final file = File(filePath);
    
        if (request.url.path == '') {
          filePath = '${appDocumentsDirectory.path}/app/index.html';
    
          final file = File(filePath);
    
          final fileContent = await file.readAsString();
    
          final headers = {"content-type": "text/html"};
    
          return Response.ok(fileContent,
              headers: headers, encoding: Encoding.getByName('utf-8'));
        } else if (path.endsWith('.js') || path.endsWith('.css')) {
          final file = File(filePath);
    
          final fileContent = await file.readAsString();
    
          final mime = path.endsWith('.js') ? 'javascript' : 'css';
    
          final headers = {"content-type": 'text/$mime'};
    
          return Response.ok(fileContent,
              headers: headers, encoding: Encoding.getByName('utf-8'));
        } else if (path.endsWith('.png')) {
          final file = File(filePath);
    
          final fileContent = await file.readAsBytes();
    
          return Response.ok(
            fileContent,
          );
        } else if (path.endsWith('.mp4')) {
          final file = File(filePath);
    
          final fileContent = await file.readAsBytes();
    
          return Response.ok(
            fileContent,
          );
        }
    
        return Response.ok('Request for "${request.url}"');
      }
    
      late WebViewController webViewController;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: FutureBuilder(
              future: initialize(),
              builder: (context, snapshot) {
                if (snapshot.connectionState != ConnectionState.done) {
                  return Container();
                }
    
                final address = server!.address.address;
    
                var htmlPath = 'http://$address:${server!.port}';
    
                // var htmlPath = 'file://${appDocumentsDirectory.path}/app/playvideo.html';
    
                return Column(
                  children: [
                    Expanded(
                      child: WebView(
                        backgroundColor: Colors.white,
                        javascriptMode: JavascriptMode.unrestricted,
                        onWebViewCreated: (controller) async {
                          webViewController = controller;
                          await controller.loadUrl(htmlPath);
                        },
                        initialMediaPlaybackPolicy:
                            AutoMediaPlaybackPolicy.always_allow,
                      ),
                    )
                  ],
                );
              }),
        );
      }
    }