Search code examples
fluttervideo-processingisolate

Is is possible to use Flutter plugins in an isolate?


I was using VideoCompress plugin with success. But since I moved my video processing to an isolate, I am getting this error:

Cannot set the method call handler before the binary messenger has been initialized. This happens when you call setMethodCallHandler() before the WidgetsFlutterBinding has been initialized. You can fix this by either calling WidgetsFlutterBinding.ensureInitialized() before this or by passing a custom BinaryMessenger instance to MethodChannel().

Though I don't see why being in an isolate could end up with an error.

So the question is, beyond video_compress, can one use a Flutter plugin from within an isolate?

Anyway, I would be grateful to anyone that might help me on this.

My code's as follows:


    late Subscription _subscription;

    // === Compressing
    try {
      isolateSwitchToState(RecordingState.compressingVideo);

      // == Subscribe to progress
      _subscription = VideoCompress.compressProgress$.subscribe((progress) {
        debugPrint('progress: $progress');
        _send2main?.send(progress);
      });

      final videoDurationInSeconds = (videoMediaInfo.duration! / 1000).floor();
      final trimDurationInSec =
          min(purchasedDurationInSeconds, videoDurationInSeconds);

      finalMediaInfo = await VideoCompress.compressVideo(videoLocalPath,
          startTime: 0,
          // Possibly truncate the video to the purchased time extension: but it cannot exceed the initial duration, hence the min
          duration: videoDurationInSeconds - trimDurationInSec,
          quality: //VideoQuality.DefaultQuality // 58MiB
              //VideoQuality.Res640x480Quality  // 17MiB
              VideoQuality.Res960x540Quality // 34MiB
          //VideoQuality.Res1280x720Quality // 58MiB
          //VideoQuality.Res1920x1080Quality // 127MiB
          //VideoQuality.HighestQuality,
          );

      isolateSwitchToState(RecordingState.videoCompressed);

      // ==Now uploading
      if (finalMediaInfo == null || finalMediaInfo.file == null) {
        isolateSwitchToErrorState(
            i18n_The_compressed_video_file_or_file_info_is_null.i18n);
        return;
      }
    } catch (e) {
      isolateSwitchToErrorState(
          i18n_While_compressing_to_the_video_file_s.i18n.fill([e.toString()]));
      return;
    } finally {
      // == Unsuscribe from progression
      _subscription.unsubscribe();
    }

[UDPATE]

The throwing is from this code:

/// Sets a callback for receiving method calls on this channel.
  ///
  /// The given callback will replace the currently registered callback for this
  /// channel, if any. To remove the handler, pass null as the
  /// `handler` argument.
  ///
  /// If the future returned by the handler completes with a result, that value
  /// is sent back to the platform plugin caller wrapped in a success envelope
  /// as defined by the [codec] of this channel. If the future completes with
  /// a [PlatformException], the fields of that exception will be used to
  /// populate an error envelope which is sent back instead. If the future
  /// completes with a [MissingPluginException], an empty reply is sent
  /// similarly to what happens if no method call handler has been set.
  /// Any other exception results in an error envelope being sent.
  void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
    assert(
      _binaryMessenger != null || BindingBase.debugBindingType() != null,
      'Cannot set the method call handler before the binary messenger has been initialized. '
      'This happens when you call setMethodCallHandler() before the WidgetsFlutterBinding '
      'has been initialized. You can fix this by either calling WidgetsFlutterBinding.ensureInitialized() '
      'before this or by passing a custom BinaryMessenger instance to MethodChannel().',
    );

From this line in compress_mixin.dart:


class CompressMixin {
  final compressProgress$ = ObservableBuilder<double>();
  final _channel = const MethodChannel('video_compress');

  @protected
  void initProcessCallback() {
    _channel.setMethodCallHandler(_progressCallback);  // <<===== From here
  }

  MethodChannel get channel => _channel;

  bool _isCompressing = false;

  bool get isCompressing => _isCompressing;

  @protected
  void setProcessingStatus(bool status) {
    _isCompressing = status;
  }

  Future<void> _progressCallback(MethodCall call) async {
    switch (call.method) {
      case 'updateProgress':
        final progress = double.tryParse(call.arguments.toString());
        if (progress != null) compressProgress$.next(progress);
        break;
    }
  }
}

Solution

  • Ok, here is the reason: plugins require special bindings and this is why I get the exception.

    Someone wrote the flutter_isolate plugin that does just that: make the binding for isolates.

    You simply replace you isolate.spawnby FlutterIsolate.spawn and add the following decoration to your global or static entry point @pragma('vm:entry-point') as follows:

    /// the code to run the compression from within the isolate   
    Future<void> runVideoCompressionFromIsolate () async {
       final isolate = await FlutterIsolate.spawn<int>(
        
         longTaskVideoCompressionForIsolate,
         7,
    
         //OPTIONS NOT AVAILABLE IN NEW PLUGIN
         //errorsAreFatal: true, // uncaught errors will terminate the isolate
         //debugName: 'longTaskVideoCompressionForIsolate', // name in debuggers and logging
       );
       
       await Future<void>.delayed(const Duration(seconds: 20));
    
       isolate.kill(); // Kill the isolate.
    }
    
    /// The entry point of our isolate.
    @pragma('vm:entry-point')
    void longTaskVideoCompressionForIsolate(int payload) async {
      try {
        // == Subscribe to progress
        final _subscription = VideoCompress.compressProgress$.subscribe((progress) {
            debugPrint('progress: $progress'); 
          });
        
          // blah blah
         }
      catch(e) {
        debugPrint(e.toString());
      }
    }
    
    

    One important note though: flutter_isolate does not allow passing objects thru ports: You must use Map<String, dynamic> instead, so be prepared for some refactors.