import 'dart:async'; import 'dart:math'; import 'package:aves/model/app/support.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/service_policy.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/byte_receiving_codec.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; abstract class MediaFetchService { Future getEntry(String uri, String? mimeType); Future getSvg( String uri, String mimeType, { required int? sizeBytes, BytesReceivedCallback? onBytesReceived, }); Future getImage( String uri, String mimeType, { required int? rotationDegrees, required bool isFlipped, required int? pageId, required int? sizeBytes, BytesReceivedCallback? onBytesReceived, }); // `rect`: region to decode, with coordinates in reference to `imageSize` Future getRegion( String uri, String mimeType, int rotationDegrees, bool isFlipped, int sampleSize, Rectangle regionRect, Size imageSize, { required int? pageId, required int? sizeBytes, Object? taskKey, int? priority, }); Future getThumbnail({ required String uri, required String mimeType, required int rotationDegrees, required int? pageId, required bool isFlipped, required int? dateModifiedSecs, required double extent, Object? taskKey, int? priority, }); Future clearSizedThumbnailDiskCache(); bool cancelRegion(Object taskKey); bool cancelThumbnail(Object taskKey); Future? resumeLoading(Object taskKey); } class PlatformMediaFetchService implements MediaFetchService { static const _platformObject = MethodChannel('deckers.thibault/aves/media_fetch_object'); static const _platformBytes = MethodChannel('deckers.thibault/aves/media_fetch_bytes', AvesByteReceivingMethodCodec()); static final _byteStream = StreamsChannel('deckers.thibault/aves/media_byte_stream'); static const double _thumbnailDefaultSize = 64.0; @override Future getEntry(String uri, String? mimeType) async { try { final result = await _platformObject.invokeMethod('getEntry', { 'uri': uri, 'mimeType': mimeType, }) as Map; return AvesEntry.fromMap(result); } on PlatformException catch (e, stack) { // do not report issues with media content as it is likely an obsolete Media Store entry if (!uri.startsWith('content://media/')) { await reportService.recordError(e, stack); } } return null; } @override Future getSvg( String uri, String mimeType, { required int? sizeBytes, BytesReceivedCallback? onBytesReceived, }) => getImage( uri, mimeType, rotationDegrees: 0, isFlipped: false, pageId: null, sizeBytes: sizeBytes, onBytesReceived: onBytesReceived, ); @override Future getImage( String uri, String mimeType, { required int? rotationDegrees, required bool isFlipped, required int? pageId, required int? sizeBytes, BytesReceivedCallback? onBytesReceived, }) async { try { final completer = Completer.sync(); final sink = OutputBuffer(); var bytesReceived = 0; _byteStream.receiveBroadcastStream({ 'uri': uri, 'mimeType': mimeType, 'sizeBytes': sizeBytes, 'rotationDegrees': rotationDegrees ?? 0, 'isFlipped': isFlipped, 'pageId': pageId, }).listen( (data) { final chunk = data as Uint8List; sink.add(chunk); if (onBytesReceived != null) { bytesReceived += chunk.length; try { onBytesReceived(bytesReceived, sizeBytes); } catch (error, stack) { completer.completeError(error, stack); return; } } }, onError: completer.completeError, onDone: () { sink.close(); completer.complete(sink.bytes); }, cancelOnError: true, ); // `await` here, so that `completeError` will be caught below return await completer.future; } on PlatformException catch (e, stack) { if (_isUnknownVisual(mimeType)) { await reportService.recordError(e, stack); } } return Uint8List(0); } @override Future getRegion( String uri, String mimeType, int rotationDegrees, bool isFlipped, int sampleSize, Rectangle regionRect, Size imageSize, { required int? pageId, required int? sizeBytes, Object? taskKey, int? priority, }) { return servicePolicy.call( () async { try { final result = await _platformBytes.invokeMethod('getRegion', { 'uri': uri, 'mimeType': mimeType, 'sizeBytes': sizeBytes, 'pageId': pageId, 'sampleSize': sampleSize, 'regionX': regionRect.left, 'regionY': regionRect.top, 'regionWidth': regionRect.width, 'regionHeight': regionRect.height, 'imageWidth': imageSize.width.toInt(), 'imageHeight': imageSize.height.toInt(), }); if (result != null) return result as Uint8List; } on PlatformException catch (e, stack) { if (_isUnknownVisual(mimeType)) { await reportService.recordError(e, stack); } } return Uint8List(0); }, priority: priority ?? ServiceCallPriority.getRegion, key: taskKey, ); } @override Future getThumbnail({ required String uri, required String mimeType, required int rotationDegrees, required int? pageId, required bool isFlipped, required int? dateModifiedSecs, required double extent, Object? taskKey, int? priority, }) { return servicePolicy.call( () async { try { final result = await _platformBytes.invokeMethod('getThumbnail', { 'uri': uri, 'mimeType': mimeType, 'dateModifiedSecs': dateModifiedSecs, 'rotationDegrees': rotationDegrees, 'isFlipped': isFlipped, 'widthDip': extent, 'heightDip': extent, 'pageId': pageId, 'defaultSizeDip': _thumbnailDefaultSize, 'quality': 100, }); if (result != null) return result as Uint8List; } on PlatformException catch (e, stack) { if (_isUnknownVisual(mimeType)) { await reportService.recordError(e, stack); } } return Uint8List(0); }, priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, ); } @override Future clearSizedThumbnailDiskCache() async { try { return _platformObject.invokeMethod('clearSizedThumbnailDiskCache'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } @override bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]); @override bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]); @override Future? resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); // convenience methods bool _isUnknownVisual(String mimeType) => !_knownMediaTypes.contains(mimeType) && MimeTypes.isVisual(mimeType); static const Set _knownOpaqueImages = { MimeTypes.jpeg, }; static const Set _knownVideos = { MimeTypes.v3gpp, MimeTypes.asf, MimeTypes.avi, MimeTypes.aviMSVideo, MimeTypes.aviVnd, MimeTypes.aviXMSVideo, MimeTypes.dvd, MimeTypes.flv, MimeTypes.flvX, MimeTypes.mkv, MimeTypes.mkvX, MimeTypes.mov, MimeTypes.movX, MimeTypes.mp2p, MimeTypes.mp2t, MimeTypes.mp2ts, MimeTypes.mp4, MimeTypes.mpeg, MimeTypes.ogv, MimeTypes.realVideo, MimeTypes.webm, MimeTypes.wmv, }; static final Set _knownMediaTypes = { MimeTypes.anyImage, ..._knownOpaqueImages, ...MimeTypes.alphaImages, ...MimeTypes.rawImages, ...AppSupport.undecodableImages, MimeTypes.anyVideo, ..._knownVideos, }; }