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 {
|
} else {
|
||||||
try (InputStream is = cr.openInputStream(uri)) {
|
try (InputStream is = cr.openInputStream(uri)) {
|
||||||
if (is != null) {
|
if (is != null) {
|
||||||
// TODO TLAD streaming would allow chunk events, but in practice Flutter blocks every time we send a chunk
|
streamBytes(is);
|
||||||
// streamBytes(is);
|
|
||||||
success(getBytes(is));
|
|
||||||
} else {
|
} else {
|
||||||
error("getImage-image-read-null", "failed to get image from uri=" + uri, null);
|
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 {
|
private void streamBytes(InputStream inputStream) throws IOException {
|
||||||
int bufferSize = 2 << 17; // ~250k
|
int bufferSize = 2 << 17; // 256kB
|
||||||
byte[] buffer = new byte[bufferSize];
|
byte[] buffer = new byte[bufferSize];
|
||||||
int len;
|
int len;
|
||||||
while ((len = inputStream.read(buffer)) != -1) {
|
while ((len = inputStream.read(buffer)) != -1) {
|
||||||
|
@ -133,17 +131,4 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
|
||||||
success(sub);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Uint8List> getImage(String uri, String mimeType) {
|
static Future<Uint8List> getImage(String uri, String mimeType, {int expectedContentLength, BytesReceivedCallback onBytesReceived}) {
|
||||||
try {
|
try {
|
||||||
final completer = Completer<Uint8List>.sync();
|
final completer = Completer<Uint8List>.sync();
|
||||||
final sink = _OutputBuffer();
|
final sink = _OutputBuffer();
|
||||||
|
var bytesReceived = 0;
|
||||||
byteChannel.receiveBroadcastStream(<String, dynamic>{
|
byteChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
}).listen(
|
}).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,
|
onError: completer.completeError,
|
||||||
onDone: () {
|
onDone: () {
|
||||||
sink.close();
|
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 {
|
class _OutputBuffer extends ByteConversionSinkBase {
|
||||||
List<List<int>> _chunks = <List<int>>[];
|
List<List<int>> _chunks = <List<int>>[];
|
||||||
int _contentLength = 0;
|
int _contentLength = 0;
|
||||||
|
@ -223,8 +239,8 @@ class _OutputBuffer extends ByteConversionSinkBase {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_bytes = Uint8List(_contentLength);
|
_bytes = Uint8List(_contentLength);
|
||||||
int offset = 0;
|
var offset = 0;
|
||||||
for (final List<int> chunk in _chunks) {
|
for (final chunk in _chunks) {
|
||||||
_bytes.setRange(offset, offset + chunk.length, chunk);
|
_bytes.setRange(offset, offset + chunk.length, chunk);
|
||||||
offset += chunk.length;
|
offset += chunk.length;
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ class _ThumbnailRasterImageState extends State<ThumbnailRasterImage> {
|
||||||
final imageProvider = UriImage(
|
final imageProvider = UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
expectedContentLength: entry.sizeBytes,
|
||||||
);
|
);
|
||||||
if (imageCache.statusForKey(imageProvider).keepAlive) {
|
if (imageCache.statusForKey(imageProvider).keepAlive) {
|
||||||
heroImageProvider = imageProvider;
|
heroImageProvider = imageProvider;
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui' as ui show Codec;
|
import 'dart:ui' as ui show Codec;
|
||||||
|
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
|
||||||
class UriImage extends ImageProvider<UriImage> {
|
class UriImage extends ImageProvider<UriImage> {
|
||||||
const UriImage({
|
const UriImage({
|
||||||
@required this.uri,
|
@required this.uri,
|
||||||
@required this.mimeType,
|
@required this.mimeType,
|
||||||
|
this.expectedContentLength,
|
||||||
this.scale = 1.0,
|
this.scale = 1.0,
|
||||||
}) : assert(uri != null),
|
}) : assert(uri != null),
|
||||||
assert(scale != null);
|
assert(scale != null);
|
||||||
|
|
||||||
final String uri, mimeType;
|
final String uri, mimeType;
|
||||||
|
final int expectedContentLength;
|
||||||
final double scale;
|
final double scale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -24,20 +27,37 @@ class UriImage extends ImageProvider<UriImage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter load(UriImage key, DecoderCallback decode) {
|
ImageStreamCompleter load(UriImage key, DecoderCallback decode) {
|
||||||
|
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||||
|
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode, chunkEvents),
|
||||||
scale: key.scale,
|
scale: key.scale,
|
||||||
|
chunkEvents: chunkEvents.stream,
|
||||||
informationCollector: () sync* {
|
informationCollector: () sync* {
|
||||||
yield ErrorDescription('uri=$uri, mimeType=$mimeType');
|
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);
|
assert(key == this);
|
||||||
|
|
||||||
final bytes = await ImageFileService.getImage(uri, mimeType);
|
try {
|
||||||
return await decode(bytes ?? Uint8List(0));
|
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
|
@override
|
||||||
|
|
|
@ -95,6 +95,7 @@ class ImageView extends StatelessWidget {
|
||||||
final uriImage = UriImage(
|
final uriImage = UriImage(
|
||||||
uri: entry.uri,
|
uri: entry.uri,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
|
expectedContentLength: entry.sizeBytes,
|
||||||
);
|
);
|
||||||
child = PhotoView(
|
child = PhotoView(
|
||||||
// key includes size and orientation to refresh when the image is rotated
|
// key includes size and orientation to refresh when the image is rotated
|
||||||
|
|
|
@ -98,7 +98,11 @@ class AvesVideoState extends State<AvesVideo> {
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
)
|
)
|
||||||
: Image(
|
: Image(
|
||||||
image: UriImage(uri: entry.uri, mimeType: entry.mimeType),
|
image: UriImage(
|
||||||
|
uri: entry.uri,
|
||||||
|
mimeType: entry.mimeType,
|
||||||
|
expectedContentLength: entry.sizeBytes,
|
||||||
|
),
|
||||||
width: entry.width.toDouble(),
|
width: entry.width.toDouble(),
|
||||||
height: entry.height.toDouble(),
|
height: entry.height.toDouble(),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue