attach method

Stream<Uint8List> attach(
  1. Stream<List<int>> dataResponse
)

Attach this RxPhoto to the Frame's dataResponse characteristic stream. If isRaw is true, then quality and resolution must be specified and match the raw image requested from Frame so that the correct jpeg header can be prepended.

Implementation

Stream<Uint8List> attach(Stream<List<int>> dataResponse) {
  // TODO check for illegal state - attach() already called on this RxPhoto etc?
  // might be possible though after a clean close(), do I want to prevent it?

  // the image data as a list of bytes that accumulates with each packet
  List<int> imageData = List.empty(growable: true);
  int rawOffset = 0;

  // if isRaw is true, a jpeg header must be prepended to the raw image data
  if (isRaw) {
    // fetch the jpeg header for this quality level and resolution
    String key = '${quality}_$resolution';

    if (!jpegHeaderMap.containsKey(key)) {
      throw Exception('No jpeg header found for quality level $quality and resolution $resolution - request full jpeg once before requesting raw');
    }

    // add the jpeg header bytes for this quality level (623 bytes)
    imageData.addAll(jpegHeaderMap[key]!);
  }

  // the subscription to the underlying data stream
  StreamSubscription<List<int>>? dataResponseSubs;

  // Our stream controller that transforms/accumulates the raw data into images (as bytes)
  _controller = StreamController();

  _controller!.onListen = () {
    _log.fine('ImageDataResponse stream subscribed');
    dataResponseSubs = dataResponse
        .where(
            (data) => data[0] == nonFinalChunkFlag || data[0] == finalChunkFlag)
        .listen((data) {
      if (data[0] == nonFinalChunkFlag) {
        imageData += data.sublist(1);
        rawOffset += data.length - 1;
      }
      // the last chunk has a first byte of finalChunkFlag so stop after this
      else if (data[0] == finalChunkFlag) {
        imageData += data.sublist(1);
        rawOffset += data.length - 1;

        Uint8List finalImageBytes = Uint8List.fromList(imageData);

        // if this image is a full jpeg, save the jpeg header for this quality level and resolution
        // so that it can be prepended to raw images of the same quality level and resolution
        if (!isRaw) {
          String key = '${quality}_$resolution';
          if (!jpegHeaderMap.containsKey(key)) {
            jpegHeaderMap[key] = finalImageBytes.sublist(0, 623);
          }
        }

        // When full image data is received,
        // rotate the image counter-clockwise 90 degrees to make it upright
        // unless requested otherwise (to save processing)
        if (upright) {
          image_lib.Image? im = image_lib.decodeJpg(finalImageBytes);
          im = image_lib.copyRotate(im!, angle: 270);
          // emit the rotated jpeg bytes
          _controller!.add(image_lib.encodeJpg(im));
        }
        else {
          // emit the original rotation jpeg bytes
          _controller!.add(finalImageBytes);
        }

        // clear the buffer
        imageData.clear();
        rawOffset = 0;

        // and close the stream
        _controller!.close();
      }
      _log.finer(() => 'Chunk size: ${data.length - 1}, rawOffset: $rawOffset');
    }, onDone: _controller!.close, onError: _controller!.addError);
    _log.fine('Controller being listened to');
  };

  _controller!.onCancel = () {
    _log.fine('ImageDataResponse stream unsubscribed');
    dataResponseSubs?.cancel();
    _controller!.close();
  };

  return _controller!.stream;
}