restored streaming fullscreen image, with chunk events
This commit is contained in:
parent
cf88c63e99
commit
ef49888a22
6 changed files with 55 additions and 28 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue