chromium/chromeos/ash/services/recording/gif_encoder.h

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROMEOS_ASH_SERVICES_RECORDING_GIF_ENCODER_H_
#define CHROMEOS_ASH_SERVICES_RECORDING_GIF_ENCODER_H_

#include <cstdint>

#include "base/memory/weak_ptr.h"
#include "base/threading/sequence_bound.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "chromeos/ash/services/recording/gif_encoding_types.h"
#include "chromeos/ash/services/recording/gif_file_writer.h"
#include "chromeos/ash/services/recording/lzw_pixel_color_indices_writer.h"
#include "chromeos/ash/services/recording/octree_color_quantizer.h"
#include "chromeos/ash/services/recording/recording_encoder.h"

namespace recording {

class RgbVideoFrame;

// Encapsulates a created color `quantizer` from the given `rgb_video_frame`,
// the extracted `color_palette` from it, and the `duration` spent on the whole
// operation (building the quantizer and extracting the palette), which is
// calculated based on the given `start_time`.
struct QuantizerPaletteData {
  QuantizerPaletteData(const RgbVideoFrame& rgb_video_frame,
                       base::TimeTicks start_time);
  QuantizerPaletteData(QuantizerPaletteData&&);
  QuantizerPaletteData& operator=(QuantizerPaletteData&&);
  ~QuantizerPaletteData();

  OctreeColorQuantizer quantizer;
  ColorTable color_palette;
  base::TimeDelta duration;
};

// Encapsulates encoding video frames into an animated GIF and writes the
// encoded output to a file that it creates at the given `gif_file_path`. An
// instance of this object can only be interacted with via a
// `base::SequenceBound` wrapper, which guarantees that all encoding operations
// as well as the destruction of the instance are done on the sequenced
// `blocking_task_runner` given to `Create()`. This prevents expensive encoding
// operations from blocking the main thread of the recording service, on which
// the video frames are delivered.
class GifEncoder : public RecordingEncoder {
 private:
  using PassKey = base::PassKey<GifEncoder>;

 public:
  // Creates an instance of this class that is bound to the given sequenced
  // `blocking_task_runner` on which all operations as well as the destruction
  // of the instance will happen. `video_encoder_options` will be used to
  // initialize the encoder upon construction. The output of GIF encoding will
  // be written directly to a file created at the given `gif_file_path`. If
  // `drive_fs_quota_delegate` is provided, that means the file `gif_file_path`
  // lives on DriveFS, and the remaining DriveFS quota will be calculated
  // through this delegate.
  // `on_failure_callback` will be called to inform the owner of this object of
  // a failure, after which all subsequent calls to `EncodeVideo()` will be
  // ignored.
  //
  // By default, `on_failure_callback` will be called on the same sequence of
  // `blocking_task_runner` (unless the caller binds the given callback to a
  // different sequence by means of `base::BindPostTask()`).
  static base::SequenceBound<GifEncoder> Create(
      scoped_refptr<base::SequencedTaskRunner> blocking_task_runner,
      const media::VideoEncoder::Options& video_encoder_options,
      mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
      const base::FilePath& gif_file_path,
      OnFailureCallback on_failure_callback);

  // Creates and returns an object that specifies the capabilities of this
  // encoder.
  static std::unique_ptr<Capabilities> CreateCapabilities();

  GifEncoder(
      PassKey,
      const media::VideoEncoder::Options& video_encoder_options,
      mojo::PendingRemote<mojom::DriveFsQuotaDelegate> drive_fs_quota_delegate,
      const base::FilePath& gif_file_path,
      OnFailureCallback on_failure_callback);
  GifEncoder(const GifEncoder&) = delete;
  GifEncoder& operator=(const GifEncoder&) = delete;
  ~GifEncoder() override;

  // RecordingEncoder:
  void InitializeVideoEncoder(
      const media::VideoEncoder::Options& video_encoder_options) override;
  void EncodeVideo(scoped_refptr<media::VideoFrame> frame) override;
  void EncodeRgbVideo(RgbVideoFrame rgb_video_frame) override;
  EncodeAudioCallback GetEncodeAudioCallback() override;
  void FlushAndFinalize(base::OnceClosure on_done) override;

 private:
  // Writes the Logical Screen Descriptor block to the GIF file. This block
  // contains info about the dimensions of the canvas within which the images of
  // the frames will be rendered. It also contains the configuration of the
  // Global Color Table.
  // This block is written only once in the entire GIF file.
  void WriteLogicalScreenDescriptor(const gfx::Size& frame_size);

  // Writes the Netscape Application Extension block to the GIF file. This block
  // allows us to embed information about looping of the frames in the output
  // animated GIF.
  // This block is written only once in the entire GIF file.
  void WriteNetscapeExtension();

  // Writes the Graphic Control Extension block to the GIF file. This block
  // contains information about the delay after which the video frame should be
  // rendered. Hence, it uses the given `current_frame_time` to calculate this
  // delay.
  // This block repeats once per every received video frame.
  void WriteGraphicControlExtension(base::TimeTicks current_frame_time);

  // Writes the Image Descriptor block to the GIF file. It describes the bounds
  // of the frame within the total bounds of the canvas (which was defined
  // earlier in the Logical Screen Descriptor block). It also specifies whether
  // the frame has its own Local Color Table.
  // This block repeats once per every received video frame.
  void WriteImageDescriptor(const RgbVideoFrame& rgb_video_frame,
                            uint8_t color_bit_depth);

  // Writes the `color_palette_` which has the given `color_bit_depth` to the
  // GIF file. This function can be uses to write both the Global and Local
  // color tables. However, we only write Local Color Tables in this
  // implementation.
  void WriteColorPalette(uint8_t color_bit_depth);

  // Moves the quantizer and its extracted color palette from the given
  // `quantizer_data` into `color_quantizer_` and `color_palette_` respectively.
  void SetQuantizer(QuantizerPaletteData&& quantizer_data);

  // The thread pool task runner on which the color palettes are built every
  // `min_num_frames_before_palette_rebuild_` frames except for the very first
  // frame, or when `min_num_frames_before_palette_rebuild_` is `1` (in which
  // case, the palette is built synchronously on the same sequence of the
  // encoder).
  // This is needed because building a new color palette is a costly operation.
  // If we do it on the same sequence of the encoder, we will block it for a
  // long time, resulting in video frames backing up, and filling the in-flight
  // video frame pool (see `FrameSinkVideoCapturerImpl::kFramePoolCapacity`).
  // This would cause the resulting GIF to be janky as many frames would be
  // dropped.
  scoped_refptr<base::SequencedTaskRunner> color_palette_task_runner_;

  // The number of frames received so far.
  size_t frame_count_ = 0;

  // The minimum number of frames that must be received before a new color
  // palette can be rebuilt. This value is dynamic and is calculated every time
  // a new color palette is built taking into account the duration of the
  // operation. The value is directly proportional with how long it took to
  // build the most recent color palette on this device (i.e. the longer it
  // takes, the larger the number of frames we should wait before building a new
  // one). We clamp it to a value between 1, and
  // `kMaxNumFramesWithStaleColorPalette`. See `SetQuantizer()`.
  int min_num_frames_before_palette_rebuild_ = 1;

  // The presentation time of the most recent video frame prior to the one being
  // encoded at the moment.
  base::TimeTicks last_frame_time_;

  // The color quantizer which we use to build the color palette and find the
  // indices of the closest colors in that palette for each pixel in the video
  // frames we're encoding.
  OctreeColorQuantizer color_quantizer_;

  // The list of colors (up to 256 colors) that will be written to the GIF file
  // as the color table. These colors are extracted from each received frame
  // using some color quantization algorithm of choice, which tries to pick the
  // most important colors that represent the contents of the video frame.
  ColorTable color_palette_;

  // The color of each pixel in the received video frame is represented as an
  // index into `color_palette_` after color quantization is complete.
  ColorIndices pixel_color_indices_;

  // Abstracts writing bytes to the GIF file, and takes care of handling IO
  // errors and remaining disk space / DriveFS quota issues.
  GifFileWriter gif_file_writer_;

  // Abstracts encoding the video frame's image color indices using the
  // Variable-Length-Code LZW compression algorithm and writing the output
  // stream to the GIF file.
  LzwPixelColorIndicesWriter lzw_encoder_;

  base::WeakPtrFactory<GifEncoder> weak_ptr_factory_{this};
};

}  // namespace recording

#endif  // CHROMEOS_ASH_SERVICES_RECORDING_GIF_ENCODER_H_