chromium/chromeos/ash/services/recording/public/mojom/recording_service.mojom

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

module recording.mojom;

import "media/mojo/mojom/audio_stream_factory.mojom";
import "mojo/public/mojom/base/big_string.mojom";
import "mojo/public/mojom/base/file_path.mojom";
import "sandbox/policy/mojom/context.mojom";
import "sandbox/policy/mojom/sandbox.mojom";
import "services/viz/privileged/mojom/compositing/frame_sink_video_capture.mojom";
import "services/viz/public/mojom/compositing/frame_sink_id.mojom";
import "services/viz/public/mojom/compositing/subtree_capture_id.mojom";
import "ui/gfx/geometry/mojom/geometry.mojom";
import "ui/gfx/image/mojom/image.mojom";

// Defines the status of recording to be provided to the client at the end of
// recording.
enum RecordingStatus {
  // Ended successfully with no errors.
  kSuccess,
  // Ended due to the recording service process was being terminated while
  // recording is still in progress (which is considered a failure).
  kServiceClosing,
  // Ended due to a failure when the mojo pipe to the video capturer on Viz was
  // disconnected.
  kVizVideoCapturerDisconnected,
  // Ended due to a failure to initialize the audio encoder.
  kAudioEncoderInitializationFailure,
  // Ended due to a failure to initialize the video encoder.
  kVideoEncoderInitializationFailure,
  // Ended due to an error occurring while encoding audio.
  kAudioEncodingError,
  // Ended due to an error occurring while encoding video.
  kVideoEncodingError,
  // Ended due to an IO error causing a failure to write to the output file.
  kIoError,
  // Ended due to reaching a critically low disk space.
  kLowDiskSpace,
  // Ended due to reaching a critically low DriveFS quota.
  kLowDriveFsQuota,
  // Ended due to the video encoder not supporting mid-recording
  // re-configuation.
  kVideoEncoderReconfigurationFailure,
};

// Defines the interface for retrieving the remaining amount of DriveFS free
// space. The implementation of this interface lives in ash, and a remote end
// point to it will be provided to the recording service only when the video
// file is being written to a path on DriveFS. The implementation of this
// delegate lives in ash-chrome (DriveFS integration lives in Chrome and cannot
// be used directly by the recording service).
interface DriveFsQuotaDelegate {
  // Gets the free space available in Drive (see
  // `drivefs.mojom.QuotaUsage.free_cloud_bytes`). `free_remaining_bytes` will
  // be set to -1 if there's an error in computing the DriveFS quota.
  GetDriveFsFreeSpaceBytes() => (int64 free_remaining_bytes);
};

// Defines the interface for the recording service client (e.g. ash), which
// lives in a remote process other than that of the recording service itself.
// The recording service consumes this interface to communicate with the client.
interface RecordingServiceClient {
  // Called by the service to inform the client that an in-progress video
  // recording ended. If |status| is anything other than |kSuccess|, then
  // recording is being terminated by the service itself due to an internal
  // failure. The service can provide an RGB image |thumbnail| representing
  // the recorded video. It can be empty in case of a failure.
  OnRecordingEnded(RecordingStatus status, gfx.mojom.ImageSkia? thumbnail);
};

// Defines the interface of the recording service which is implemented by
// |recording::RecordingService| and runs in its own utility process. It is
// launched by Ash on which the |RecordingServiceClient| resides, and is used to
// perform audio/video recording of the whole screen, a partial region of it, or
// an individual window. The service captures, encodes, and muxes the audio and
// video frames, and writes the output directly to a file whose path was given
// to the Record*() functions. The extension of the path (e.g. ".webm" or
// ".gif") will determine how the services encodes the video frames and
// generates the output. Audio recording is allowed only when recording a WebM
// video.
// Note that a maximum of one screen recording can be done at any time.
// TODO(https://crbug.com/1147991) Explore alternative sandboxing.
[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox,
 RequireContext=sandbox.mojom.Context.kBrowser]
interface RecordingService {
  // All the below Record*() interfaces, take a pending remote to a client (e.g.
  // Ash) which will be notified when recording finishes and all the encoded
  // chunks are written to the file, a pending remote bound to the video
  // capturer on Viz on the GPU process, another *optional* pending remote
  // to the audio stream factory on the Audio Service, which if not provided,
  // the service will not record audio, and a path to the output file where the
  // video content should be saved. Only ".webm" and ".gif" are currently
  // supported.
  //
  // The *optional* pending_remotes `microphone_stream_factory` and
  // `system_audio_stream_factory` should be provided if the microphone audio
  // and system audio are desired to be recorded respectively. The following
  // cases are possible:
  // - Both `microphone_stream_factory` and `system_audio_stream_factory` are
  //   not bound: this means no audio recording is desired.
  // - `microphone_stream_factory` is bound: this means only microphone audio is
  //   desired to be recorded.
  // - `system_audio_stream_factory` is bound: this means only system audio is
  //   desired to be recorded.
  // - Both `microphone_stream_factory` and `system_audio_stream_factory` are
  //   bound: this means both microphone and system audio will be recorded at
  //   the same time and will be mixed together to produce a single audio stream
  //   that will be muxed with the video.
  //
  // The *optional* pending_remote `drive_fs_quota_delegate`, if provided, that
  // means that the given `output_file_path` lives on DriveFS, and the remaining
  // free space calculations has to be done through this delegate.
  //
  // Note that we pass a FilePath rather than a File, since:
  // - Passing a File means the caller has to create and open the file itself,
  //   which has to be done asynchronously on the ash UI side, resulting in a
  //   longer delay before recording can start. However, this wasn't a major
  //   concern.
  // - The recording service process is not sandboxed, so it can create and open
  //   the file itself.
  // - Since the recording service will be writing to the file directly, it's
  //   easier to poll the remaining disk space after write operations, rather
  //   than relying on the caller to repeatedly do this polling.
  // TODO(https://crbug.com/1211480): Explore a different architecture if
  // possible.

  // Starts a fullscreen recording of a root window which has the given
  // |frame_sink_id|. Using the given |frame_sink_size_dip| and
  // |device_scale_factor|, the resulting video will have a resolution equal to
  // the pixel size of the recorded frame sink. Note that calling
  // OnFrameSinkSizeChanged() will do nothing in this recording mode, and the
  // output video will maintain the same size as the initial pixel size of the
  // frame sink. The content of the video will letterbox within that size as
  // needed. This is desired in Fullscreen recording, as it makes it clear when
  // the recorded display changes its rotation.
  // |frame_sink_id| must be valid.
  [AllowedContext=sandbox.mojom.Context.kBrowser]
  RecordFullscreen(
      pending_remote<RecordingServiceClient> client,
      pending_remote<viz.mojom.FrameSinkVideoCapturer> video_capturer,
      pending_remote<media.mojom.AudioStreamFactory>? microphone_stream_factory,
      pending_remote<media.mojom.AudioStreamFactory>?
          system_audio_stream_factory,
      pending_remote<DriveFsQuotaDelegate>? drive_fs_quota_delegate,
      mojo_base.mojom.FilePath output_file_path,
      viz.mojom.FrameSinkId frame_sink_id,
      gfx.mojom.Size frame_sink_size_dip,
      float device_scale_factor);

  // Starts a recording of a window. If this window has a valid |frame_sink_id|,
  // and submits its own compositor frames independently, then
  // |subtree_capture_id| can be default-constructed and won't be used.
  // Otherwise, for windows that don't submit compositor frames (e.g. non-root
  // windows), the given |frame_sink_id| must be of the root window they're
  // descendant from, and they must be made capturable by tagging the them with
  // a valid |subtree_capture_id| before calling this (see
  // aura::Window::MakeWindowCapturable()).
  // |window_size_dip| is the initial size of the recorded window, and can be
  // updated by calling OnRecordedWindowSizeChanged() when the window gets
  // resized by the user (e.g. resized, maximized, fullscreened, ... etc.).
  // Unlike fullscreen recording, updating the window size will update the
  // output video size, since it is desired for the video dimensions to always
  // match those of the window in pixels without letterboxing. Hence, the
  // current |device_scale_factor| is provided to perform the dip-to-pixel
  // conversion. |frame_sink_size_dip| is the current size of the frame sink
  // whose |frame_sink_id| was given. This can be updated by calling
  // OnFrameSinkSizeChanged() which will update the resolution constraints on
  // the capturer to avoid letterboxing, so the resulting video frames are sharp
  // and crisp, and their size match that of the window in pixels.
  [AllowedContext=sandbox.mojom.Context.kBrowser]
  RecordWindow(
      pending_remote<RecordingServiceClient> client,
      pending_remote<viz.mojom.FrameSinkVideoCapturer> video_capturer,
      pending_remote<media.mojom.AudioStreamFactory>? microphone_stream_factory,
      pending_remote<media.mojom.AudioStreamFactory>?
          system_audio_stream_factory,
      pending_remote<DriveFsQuotaDelegate>? drive_fs_quota_delegate,
      mojo_base.mojom.FilePath output_file_path,
      viz.mojom.FrameSinkId frame_sink_id,
      gfx.mojom.Size frame_sink_size_dip,
      float device_scale_factor,
      viz.mojom.SubtreeCaptureId subtree_capture_id,
      gfx.mojom.Size window_size_dip);

  // Starts a recording of a partial region of a root window which has the given
  // |frame_sink_id|. Using the given |frame_sink_size_dip| and
  // |device_scale_factor|, the video will be captured at a resolution equal to
  // the size of the frame sink in pixels, but the resulting video frames will
  // be cropped to the pixel bounds that corresponds to the given
  // |crop_region_dip|.
  // If the root window gets resized (due to e.g. rotation, device scale factor
  // change ... etc.) OnFrameSinkSizeChanged() must be called to update the
  // resolution constraints on the capturer in order to avoid letterboxing.
  // Letterboxing breaks the relation between the crop region bounds and
  // generated video frame sizes, which results in the wrong region being shown
  // in the video.
  // |frame_sink_id| must be valid.
  [AllowedContext=sandbox.mojom.Context.kBrowser]
  RecordRegion(
      pending_remote<RecordingServiceClient> client,
      pending_remote<viz.mojom.FrameSinkVideoCapturer> video_capturer,
      pending_remote<media.mojom.AudioStreamFactory>? microphone_stream_factory,
      pending_remote<media.mojom.AudioStreamFactory>?
          system_audio_stream_factory,
      pending_remote<DriveFsQuotaDelegate>? drive_fs_quota_delegate,
      mojo_base.mojom.FilePath output_file_path,
      viz.mojom.FrameSinkId frame_sink_id,
      gfx.mojom.Size frame_sink_size_dip,
      float device_scale_factor,
      gfx.mojom.Rect crop_region_dip);

  // Stops an on-going video recording. The remaining frames will continue to be
  // written to the output file until OnRecordingEnded() is called on the
  // client.
  StopRecording();

  // Called by the client to let the service know that a window being recorded
  // is about to move to a different display (i.e a different root window),
  // giving it the |new_frame_sink_id| of that new root window. It also provides
  // the |new_frame_sink_size_dip| and |new_device_scale_factor| (since the new
  // display may have different bounds).
  // Note that changing the window's root doesn't affect the window's
  // SubtreeCaptureId, and therefore it doesn't need to be reprovided.
  // This can *only* be called when the service is already recording a window
  // (i.e. RecordWindow() has already been called to start the recording of a
  // window).
  OnRecordedWindowChangingRoot(
      viz.mojom.FrameSinkId new_frame_sink_id,
      gfx.mojom.Size new_frame_sink_size_dip,
      float new_device_scale_factor);

  // Called by the client to let the service know that a window being recorded
  // has been resized (e.g. due to snapping, maximizing, user resizing, ...
  // etc.). |new_window_size_dip| is the new size in DIPs which will be used to
  // change the dimensions of the output video to match the new size in pixels
  // according to the current value of the device scale factor. Therefore, it's
  // better to throttle such changes to avoid having to change the size of the
  // video repeatedly.
  // This can *only* be called when the service is already recording a window
  // (i.e. RecordWindow() has already been called to start the recording of a
  // window).
  OnRecordedWindowSizeChanged(gfx.mojom.Size new_window_size_dip);

  // Called by the client to let the service know that the frame sink being
  // recorded (whose |frame_sink_id| was provided to the above functions) has
  // changed its size to or its device scale factor to |new_frame_sink_size_dip|
  // or |new_device_scale_factor| respectively, resulting in a potential change
  // in the resolution of the output video.
  // This does nothing when recording a fullscreen display, as the capturer
  // already takes care of centering and letter-boxing the video content within
  // the initial requested video resolution.
  OnFrameSinkSizeChanged(gfx.mojom.Size new_frame_sink_size_dip,
      float new_device_scale_factor);
};