Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add output/:output_id/request_keyframe endpoint #620

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- Support DeckLink cards as an input. ([#587](https://github.com/membraneframework/live_compositor/pull/587), [#597](https://github.com/membraneframework/live_compositor/pull/597), [#598](https://github.com/membraneframework/live_compositor/pull/598), [#599](https://github.com/membraneframework/live_compositor/pull/599) by [@wkozyra95](https://github.com/wkozyra95))
- Add `LIVE_COMPOSITOR_INPUT_BUFFER_DURATION_MS` environment variable to control input stream buffer size. ([#600](https://github.com/membraneframework/live_compositor/pull/600) by [@wkozyra95](https://github.com/wkozyra95))
- Add endpoint for requesting keyframe on the output stream. ([#620](https://github.com/membraneframework/live_compositor/pull/620) by [@WojciechBarczynski](https://github.com/WojciechBarczynski))

### 🐛 Bug fixes

Expand Down
15 changes: 13 additions & 2 deletions compositor_pipeline/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use compositor_render::{
error::{
InitRendererEngineError, RegisterError, RegisterRendererError, UnregisterRendererError,
UpdateSceneError, WgpuError,
InitRendererEngineError, RegisterError, RegisterRendererError, RequestKeyframeError,
UnregisterRendererError, UpdateSceneError, WgpuError,
},
InputId, OutputId,
};
Expand Down Expand Up @@ -243,6 +243,17 @@ impl From<&UpdateSceneError> for PipelineErrorInfo {
}
}

const REQUEST_KEYFRAME_ERROR: &str = "REQUEST_KEYFRAME_ERROR";

impl From<&RequestKeyframeError> for PipelineErrorInfo {
fn from(_err: &RequestKeyframeError) -> Self {
PipelineErrorInfo {
error_code: REQUEST_KEYFRAME_ERROR,
error_type: ErrorType::UserError,
}
}
}

const WGPU_INIT_ERROR: &str = "WGPU_INIT_ERROR";
const WEB_RENDERER_INIT_ERROR: &str = "WEB_RENDERER_INIT_ERROR";
const LAYOUT_INIT_ERROR: &str = "LAYOUT_INIT_ERROR";
Expand Down
11 changes: 10 additions & 1 deletion compositor_pipeline/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use std::thread;
use std::time::Duration;

use compositor_render::error::{
ErrorStack, InitPipelineError, RegisterRendererError, UnregisterRendererError,
ErrorStack, InitPipelineError, RegisterRendererError, RequestKeyframeError,
UnregisterRendererError,
};
use compositor_render::scene::Component;
use compositor_render::web_renderer::WebRendererInitOptions;
Expand Down Expand Up @@ -268,6 +269,14 @@ impl Pipeline {
Ok(())
}

pub fn request_keyframe(&self, output_id: OutputId) -> Result<(), RequestKeyframeError> {
let Some(output) = self.outputs.get(&output_id) else {
return Err(RequestKeyframeError::OutputNotRegistered(output_id.clone()));
};

output.output.request_keyframe(output_id)
}

fn check_output_spec(
&self,
output_id: &OutputId,
Expand Down
6 changes: 6 additions & 0 deletions compositor_pipeline/src/pipeline/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ impl VideoEncoder {
Self::H264(encoder) => encoder.resolution(),
}
}

pub fn request_keyframe(&self) {
match self {
Self::H264(encoder) => encoder.request_keyframe(),
}
}
}

impl AudioEncoder {
Expand Down
15 changes: 15 additions & 0 deletions compositor_pipeline/src/pipeline/encoder/ffmpeg_h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub struct Options {
pub struct LibavH264Encoder {
resolution: Resolution,
frame_sender: Sender<PipelineEvent<Frame>>,
keyframe_req_sender: Sender<()>,
}

impl LibavH264Encoder {
Expand All @@ -98,6 +99,7 @@ impl LibavH264Encoder {
) -> Result<Self, EncoderInitError> {
let (frame_sender, frame_receiver) = crossbeam_channel::bounded(5);
let (result_sender, result_receiver) = crossbeam_channel::bounded(0);
let (keyframe_req_sender, keyframe_req_receiver) = crossbeam_channel::unbounded();

let options_clone = options.clone();
let output_id = output_id.clone();
Expand All @@ -114,6 +116,7 @@ impl LibavH264Encoder {
let encoder_result = run_encoder_thread(
options_clone,
frame_receiver,
keyframe_req_receiver,
chunks_sender,
&result_sender,
);
Expand All @@ -133,6 +136,7 @@ impl LibavH264Encoder {
Ok(Self {
frame_sender,
resolution: options.resolution,
keyframe_req_sender,
})
}

Expand All @@ -143,11 +147,18 @@ impl LibavH264Encoder {
pub fn resolution(&self) -> Resolution {
self.resolution
}

pub fn request_keyframe(&self) {
if let Err(err) = self.keyframe_req_sender.send(()) {
debug!(%err, "Failed to send keyframe request to the encoder.");
}
}
}

fn run_encoder_thread(
options: Options,
frame_receiver: Receiver<PipelineEvent<Frame>>,
keyframe_req_receiver: Receiver<()>,
packet_sender: Sender<EncoderOutputEvent>,
result_sender: &Sender<Result<(), EncoderInitError>>,
) -> Result<(), EncoderInitError> {
Expand Down Expand Up @@ -221,6 +232,10 @@ fn run_encoder_thread(
continue;
}

if keyframe_req_receiver.try_recv().is_ok() {
av_frame.set_kind(ffmpeg_next::picture::Type::I);
}

if let Err(e) = encoder.send_frame(&av_frame) {
error!("Encoder error: {e}.");
continue;
Expand Down
20 changes: 19 additions & 1 deletion compositor_pipeline/src/pipeline/output.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use compositor_render::{Frame, OutputFrameFormat, OutputId, Resolution};
use compositor_render::{
error::RequestKeyframeError, Frame, OutputFrameFormat, OutputId, Resolution,
};
use crossbeam_channel::{bounded, Receiver, Sender};

use crate::{audio_mixer::OutputSamples, error::RegisterOutputError, queue::PipelineEvent};
Expand Down Expand Up @@ -180,6 +182,22 @@ impl Output {
}
}

pub fn request_keyframe(&self, output_id: OutputId) -> Result<(), RequestKeyframeError> {
let encoder = match &self {
Output::Rtp { encoder, .. } => encoder,
Output::EncodedData { encoder } => encoder,
Output::RawData { .. } => return Err(RequestKeyframeError::RawOutput(output_id)),
};

encoder
.video
.as_ref()
.ok_or(RequestKeyframeError::NoVideoOutput(output_id))?
.request_keyframe();

Ok(())
}

pub(super) fn output_frame_format(&self) -> Option<OutputFrameFormat> {
match &self {
Output::Rtp { encoder, .. } => encoder
Expand Down
12 changes: 12 additions & 0 deletions compositor_render/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ pub enum UpdateSceneError {
AudioVideoNotMatching(OutputId),
}

#[derive(Debug, thiserror::Error)]
pub enum RequestKeyframeError {
#[error("Output \"{0}\" does not exist, register it first before requesting keyframe.")]
OutputNotRegistered(OutputId),
#[error(
"Output \"{0}\" is a raw output. Keyframe request is only available for encoded outputs."
)]
RawOutput(OutputId),
#[error("Output \"{0}\" is not a video output. Can't request a keyframe on non video output.")]
NoVideoOutput(OutputId),
}

pub struct ErrorStack<'a>(Option<&'a (dyn std::error::Error + 'static)>);

impl<'a> ErrorStack<'a> {
Expand Down
12 changes: 12 additions & 0 deletions docs/pages/api/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ Update scene definition and audio mixer configuration for output with ID `:outpu

***

### Request keyframe

```http
POST: /api/output/:output_id/request_keyframe
```

```typescript
type RequestBody = {}
```

Requests additional keyframe (I frame) on the video output.

## Inputs configuration

### Register input
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/concept/shaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ You can define a custom WGSL struct and bind a value of this type as
@group(1) @binding(0) var<uniform> custom_name: CustomStruct;
```

This struct has to be provided when creating a node using the `shader_params` field of the [shader node struct](https://github.com/membraneframework/live_compositor/wiki/API-%E2%80%90-nodes#shader)
This struct has to be provided when creating a node using the `shader_params` field of the [shader node struct](./../api/components/Shader.md#shader).

### Entrypoints

Expand All @@ -128,4 +128,4 @@ fn fs_main(input: A) -> @location(0) vec4<f32>

Where `A` is the output type of the vertex shader.

Shaders have to be registered using the [register shader](https://github.com/membraneframework/live_compositor/wiki/Api-%E2%80%90-renderers#shader) request before they can be used.
Shaders have to be registered using the [register shader](./../api/routes.md#register-shader) request before they can be used.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated URL

8 changes: 6 additions & 2 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use crate::{
state::{ApiState, Response},
};

use self::{update_output::handle_output_update, ws::handle_ws_upgrade};
use self::{
update_output::handle_keyframe_request, update_output::handle_output_update,
ws::handle_ws_upgrade,
};

mod register_request;
mod unregister_request;
Expand All @@ -34,7 +37,8 @@ pub fn routes(state: ApiState) -> Router {
let outputs = Router::new()
.route("/:id/register", post(register_request::handle_output))
.route("/:id/unregister", post(unregister_request::handle_output))
.route("/:id/update", post(handle_output_update));
.route("/:id/update", post(handle_output_update))
.route("/:id/request_keyframe", post(handle_keyframe_request));

let image = Router::new()
.route("/:id/register", post(register_request::handle_image))
Expand Down
9 changes: 9 additions & 0 deletions src/routes/update_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,12 @@ pub(super) async fn handle_output_update(
};
Ok(Response::Ok {})
}

pub(super) async fn handle_keyframe_request(
State(api): State<ApiState>,
Path(output_id): Path<OutputId>,
) -> Result<Response, ApiError> {
api.pipeline().request_keyframe(output_id.into())?;

Ok(Response::Ok {})
}