// 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 client, pending_remote video_capturer, pending_remote? microphone_stream_factory, pending_remote? system_audio_stream_factory, pending_remote? 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 client, pending_remote video_capturer, pending_remote? microphone_stream_factory, pending_remote? system_audio_stream_factory, pending_remote? 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 client, pending_remote video_capturer, pending_remote? microphone_stream_factory, pending_remote? system_audio_stream_factory, pending_remote? 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); };