From ef49888a22703e24339ed98987dc0bd5cbc21cc1 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 5 Jun 2020 11:42:42 +0900 Subject: [PATCH] restored streaming fullscreen image, with chunk events --- .../ImageByteStreamHandler.java | 19 ++---------- lib/services/image_file_service.dart | 26 ++++++++++++---- lib/widgets/album/thumbnail/raster.dart | 1 + .../image_providers/uri_image_provider.dart | 30 +++++++++++++++---- lib/widgets/fullscreen/image_view.dart | 1 + lib/widgets/fullscreen/video_view.dart | 6 +++- 6 files changed, 55 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java index 96a57df08..929f228e8 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java @@ -108,9 +108,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { } else { try (InputStream is = cr.openInputStream(uri)) { if (is != null) { - // TODO TLAD streaming would allow chunk events, but in practice Flutter blocks every time we send a chunk -// streamBytes(is); - success(getBytes(is)); + streamBytes(is); } else { error("getImage-image-read-null", "failed to get image from uri=" + uri, null); } @@ -123,7 +121,7 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { } private void streamBytes(InputStream inputStream) throws IOException { - int bufferSize = 2 << 17; // ~250k + int bufferSize = 2 << 17; // 256kB byte[] buffer = new byte[bufferSize]; int len; while ((len = inputStream.read(buffer)) != -1) { @@ -133,17 +131,4 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler { success(sub); } } - - // InputStream.readAllBytes is only available from Java 9+ - private byte[] getBytes(InputStream inputStream) throws IOException { - ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); - int bufferSize = 1024; - byte[] buffer = new byte[bufferSize]; - - int len; - while ((len = inputStream.read(buffer)) != -1) { - byteBuffer.write(buffer, 0, len); - } - return byteBuffer.toByteArray(); - } } diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 0e381a2fb..d4dbe37af 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -36,15 +36,28 @@ class ImageFileService { return null; } - static Future getImage(String uri, String mimeType) { + static Future getImage(String uri, String mimeType, {int expectedContentLength, BytesReceivedCallback onBytesReceived}) { try { final completer = Completer.sync(); final sink = _OutputBuffer(); + var bytesReceived = 0; byteChannel.receiveBroadcastStream({ 'uri': uri, 'mimeType': mimeType, }).listen( - (chunk) => sink.add(chunk as Uint8List), + (data) { + final chunk = data as Uint8List; + sink.add(chunk); + if (onBytesReceived != null) { + bytesReceived += chunk.length; + try { + onBytesReceived(bytesReceived, expectedContentLength); + } catch (error, stackTrace) { + completer.completeError(error, stackTrace); + return; + } + } + }, onError: completer.completeError, onDone: () { sink.close(); @@ -203,7 +216,10 @@ class MoveOpEvent extends ImageOpEvent { } } -// copied from `consolidateHttpClientResponseBytes` in flutter/foundation +// cf flutter/foundation `consolidateHttpClientResponseBytes` +typedef BytesReceivedCallback = void Function(int cumulative, int total); + +// cf flutter/foundation `consolidateHttpClientResponseBytes` class _OutputBuffer extends ByteConversionSinkBase { List> _chunks = >[]; int _contentLength = 0; @@ -223,8 +239,8 @@ class _OutputBuffer extends ByteConversionSinkBase { return; } _bytes = Uint8List(_contentLength); - int offset = 0; - for (final List chunk in _chunks) { + var offset = 0; + for (final chunk in _chunks) { _bytes.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } diff --git a/lib/widgets/album/thumbnail/raster.dart b/lib/widgets/album/thumbnail/raster.dart index 601f44229..2ca5f40f7 100644 --- a/lib/widgets/album/thumbnail/raster.dart +++ b/lib/widgets/album/thumbnail/raster.dart @@ -116,6 +116,7 @@ class _ThumbnailRasterImageState extends State { final imageProvider = UriImage( uri: entry.uri, mimeType: entry.mimeType, + expectedContentLength: entry.sizeBytes, ); if (imageCache.statusForKey(imageProvider).keepAlive) { heroImageProvider = imageProvider; diff --git a/lib/widgets/common/image_providers/uri_image_provider.dart b/lib/widgets/common/image_providers/uri_image_provider.dart index 7dc101f34..4b48b831c 100644 --- a/lib/widgets/common/image_providers/uri_image_provider.dart +++ b/lib/widgets/common/image_providers/uri_image_provider.dart @@ -1,20 +1,23 @@ +import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui show Codec; import 'package:aves/services/image_file_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:pedantic/pedantic.dart'; class UriImage extends ImageProvider { const UriImage({ @required this.uri, @required this.mimeType, + this.expectedContentLength, this.scale = 1.0, }) : assert(uri != null), assert(scale != null); final String uri, mimeType; - + final int expectedContentLength; final double scale; @override @@ -24,20 +27,37 @@ class UriImage extends ImageProvider { @override ImageStreamCompleter load(UriImage key, DecoderCallback decode) { + final chunkEvents = StreamController(); + return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), + codec: _loadAsync(key, decode, chunkEvents), scale: key.scale, + chunkEvents: chunkEvents.stream, informationCollector: () sync* { yield ErrorDescription('uri=$uri, mimeType=$mimeType'); }, ); } - Future _loadAsync(UriImage key, DecoderCallback decode) async { + Future _loadAsync(UriImage key, DecoderCallback decode, StreamController chunkEvents) async { assert(key == this); - final bytes = await ImageFileService.getImage(uri, mimeType); - return await decode(bytes ?? Uint8List(0)); + try { + final bytes = await ImageFileService.getImage( + uri, + mimeType, + expectedContentLength: expectedContentLength, + onBytesReceived: (cumulative, total) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + )); + }, + ); + return await decode(bytes ?? Uint8List(0)); + } finally { + unawaited(chunkEvents.close()); + } } @override diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index 1af06bde7..86febbb54 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -95,6 +95,7 @@ class ImageView extends StatelessWidget { final uriImage = UriImage( uri: entry.uri, mimeType: entry.mimeType, + expectedContentLength: entry.sizeBytes, ); child = PhotoView( // key includes size and orientation to refresh when the image is rotated diff --git a/lib/widgets/fullscreen/video_view.dart b/lib/widgets/fullscreen/video_view.dart index d2224034a..092f4a78b 100644 --- a/lib/widgets/fullscreen/video_view.dart +++ b/lib/widgets/fullscreen/video_view.dart @@ -98,7 +98,11 @@ class AvesVideoState extends State { backgroundColor: Colors.transparent, ) : Image( - image: UriImage(uri: entry.uri, mimeType: entry.mimeType), + image: UriImage( + uri: entry.uri, + mimeType: entry.mimeType, + expectedContentLength: entry.sizeBytes, + ), width: entry.width.toDouble(), height: entry.height.toDouble(), );