restored streaming fullscreen image, with chunk events

This commit is contained in:
Thibault Deckers 2020-06-05 11:42:42 +09:00
parent cf88c63e99
commit ef49888a22
6 changed files with 55 additions and 28 deletions

View file

@ -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();
}
}

View file

@ -36,15 +36,28 @@ class ImageFileService {
return null;
}
static Future<Uint8List> getImage(String uri, String mimeType) {
static Future<Uint8List> getImage(String uri, String mimeType, {int expectedContentLength, BytesReceivedCallback onBytesReceived}) {
try {
final completer = Completer<Uint8List>.sync();
final sink = _OutputBuffer();
var bytesReceived = 0;
byteChannel.receiveBroadcastStream(<String, dynamic>{
'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<List<int>> _chunks = <List<int>>[];
int _contentLength = 0;
@ -223,8 +239,8 @@ class _OutputBuffer extends ByteConversionSinkBase {
return;
}
_bytes = Uint8List(_contentLength);
int offset = 0;
for (final List<int> chunk in _chunks) {
var offset = 0;
for (final chunk in _chunks) {
_bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}

View file

@ -116,6 +116,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
final imageProvider = UriImage(
uri: entry.uri,
mimeType: entry.mimeType,
expectedContentLength: entry.sizeBytes,
);
if (imageCache.statusForKey(imageProvider).keepAlive) {
heroImageProvider = imageProvider;

View file

@ -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<UriImage> {
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<UriImage> {
@override
ImageStreamCompleter load(UriImage key, DecoderCallback decode) {
final chunkEvents = StreamController<ImageChunkEvent>();
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<ui.Codec> _loadAsync(UriImage key, DecoderCallback decode) async {
Future<ui.Codec> _loadAsync(UriImage key, DecoderCallback decode, StreamController<ImageChunkEvent> 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

View file

@ -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

View file

@ -98,7 +98,11 @@ class AvesVideoState extends State<AvesVideo> {
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(),
);