diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 75f9c15..1b884f3 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -9,8 +9,8 @@ export DOTNET_CLI_TELEMETRY_OPTOUT=1 mkdir -p /backend/out cd /backend/src/obs_recorder -dotnet publish -r linux-x64 -c Release -o /backend/out/ -# dotnet publish -r linux-x64 -c Debug -o /backend/out/ +# dotnet publish -r linux-x64 -c Release -o /backend/out/ +dotnet publish -r linux-x64 -c Debug -o /backend/out/ #rm -rf /backend/out/obs_recorder.dbg diff --git a/backend/src/obs_net/Obs.cs b/backend/src/obs_net/Obs.cs index aae5d58..527c5da 100644 --- a/backend/src/obs_net/Obs.cs +++ b/backend/src/obs_net/Obs.cs @@ -312,7 +312,18 @@ public enum video_range_type : int public static extern float obs_source_get_volume(obs_source_t source); [DllImport(importLibrary, CallingConvention = importCall)] - public static extern string obs_source_get_name(obs_source_t source); + public static extern IntPtr obs_source_get_name(obs_source_t source); + + + public static string GetSourceName(IntPtr source) + { + IntPtr namePtr = obs_source_get_name(source); + if (namePtr != IntPtr.Zero) + { + return Marshal.PtrToStringUTF8(namePtr); // Use PtrToStringUni for UTF-16 + } + return null; + } public enum VideoResetError { diff --git a/backend/src/obs_recorder/ConfigService.cs b/backend/src/obs_recorder/ConfigService.cs index 8a2d1ff..280f1c2 100644 --- a/backend/src/obs_recorder/ConfigService.cs +++ b/backend/src/obs_recorder/ConfigService.cs @@ -51,6 +51,8 @@ internal partial class VolumePeakLevelSourceGenerationContext : JsonSerializerCo public class ConfigService { + public ConfigModel Config; + public EventHandler ConfigChanged; private readonly string ConfigFilePath = Environment.GetEnvironmentVariable("DECKY_PLUGIN_SETTINGS_DIR") + "/config.json"; private ILogger Logger { get; } @@ -60,12 +62,16 @@ public ConfigService(ILogger logger) if (!File.Exists(ConfigFilePath)) { - var config = new ConfigModel(); - _= SaveConfig(config); + Config = new ConfigModel(); + _= SaveConfig(Config); + } + else + { + Config = GetConfig(); } } - public ConfigModel GetConfig() + ConfigModel GetConfig() { Logger.LogInformation("Loading config"); try { @@ -97,6 +103,8 @@ public async Task SaveConfig(ConfigModel config) Logger.LogInformation("Saving config"); var json = JsonSerializer.Serialize(config, ConfigSourceGenerationContext.Default.ConfigModel); await File.WriteAllTextAsync(ConfigFilePath, json); + Config = config; + ConfigChanged?.Invoke(this, EventArgs.Empty); return config; } } diff --git a/backend/src/obs_recorder/ObsRecordingService.cs b/backend/src/obs_recorder/ObsRecordingService.cs index 9e4ebb0..17f48c7 100644 --- a/backend/src/obs_recorder/ObsRecordingService.cs +++ b/backend/src/obs_recorder/ObsRecordingService.cs @@ -164,10 +164,10 @@ public void Init() Initialized = true; Logger.LogInformation("Initialized"); - var config = ConfigService.GetConfig(); - Logger.LogError("Config: " + JsonSerializer.Serialize(config, ConfigSourceGenerationContext.Default.ConfigModel)); - if (config.ReplayBufferEnabled && config.ReplayBufferSeconds > 0) + Logger.LogError("Config: " + JsonSerializer.Serialize(ConfigService.Config, ConfigSourceGenerationContext.Default.ConfigModel)); + + if (ConfigService.Config.ReplayBufferEnabled && ConfigService.Config.ReplayBufferSeconds > 0) { StartBufferOutput(); } @@ -181,9 +181,9 @@ public void Init() public void StartStreaming() { - var config = ConfigService.GetConfig(); + string server = ""; - switch (config.StreamingService) + switch (ConfigService.Config.StreamingService) { case "twitch": server = "rtmp://lhr08.contribute.live-video.net/app/"; @@ -192,12 +192,12 @@ public void StartStreaming() break; default: - throw new Exception("Unknown streaming service: " + config.StreamingService); + throw new Exception("Unknown streaming service: " + ConfigService.Config.StreamingService); } IntPtr settings = obs_data_create(); - if (config.StreamingService == "whip") + if (ConfigService.Config.StreamingService == "whip") { service = obs_service_create("whip_custom", "whip_service", IntPtr.Zero, IntPtr.Zero); @@ -211,7 +211,7 @@ public void StartStreaming() server = "https://b.siobud.com/api/whip"; obs_data_set_string(settings, "service", "whip_custom"); - obs_data_set_string(settings, "bearer_token", config.StreamingKey); + obs_data_set_string(settings, "bearer_token", ConfigService.Config.StreamingKey); obs_data_set_string(settings, "server", server); obs_service_update(service, settings); @@ -223,8 +223,8 @@ public void StartStreaming() { obs_data_set_string(settings, "server", server); - obs_data_set_string(settings, "service", config.StreamingService); - obs_data_set_string(settings, "key", config.StreamingKey); + obs_data_set_string(settings, "service", ConfigService.Config.StreamingService); + obs_data_set_string(settings, "key", ConfigService.Config.StreamingKey); service = obs_service_create("rtmp_common", "rtmp_service", settings, IntPtr.Zero); obs_data_release(settings); @@ -403,27 +403,38 @@ void OnAudioData(IntPtr param, IntPtr source, ref AudioData audioData, bool mute percentage = Math.Min(Math.Max(percentage, 0.0f), 100.0f); - OnVolumePeakChanged?.Invoke(new VolumePeakLevel() { Peak = percentage, Channel = 0, Source = obs_source_get_name(source) }); + Logger.LogError("Volume: " + percentage); + + string sourceName = null; + if (source == IntPtr.Zero) + { + Logger.LogError("Source is null"); + } + else + { + sourceName = GetSourceName(source); + } + + Logger.LogError("Source: " + sourceName); + OnVolumePeakChanged?.Invoke(new VolumePeakLevel() { Peak = percentage, Source = sourceName }); } void InitVideoOut() { - var config = ConfigService.GetConfig(); - IntPtr videoSource = obs_source_create("pipewire-gamescope-capture-source", "Gamescope Capture Source", IntPtr.Zero, IntPtr.Zero); obs_set_output_source(0, videoSource); IntPtr videoEncoderSettings = obs_data_create(); - var MonitorSize = X11Interop.GetSize(); + var MonitorSize = X11Interop.GetSize(); obs_data_set_int(videoEncoderSettings, "width", (uint)MonitorSize.width); obs_data_set_int(videoEncoderSettings, "height", (uint)MonitorSize.height); - obs_data_set_int(videoEncoderSettings, "fps_num", (uint)config.FPS); + obs_data_set_int(videoEncoderSettings, "fps_num", (uint)ConfigService.Config.FPS); - if (config.Encoder == "obs_x264") + if (ConfigService.Config.Encoder == "obs_x264") { obs_data_set_int(videoEncoderSettings, "bitrate", 3500); @@ -432,7 +443,7 @@ void InitVideoOut() obs_data_set_string(videoEncoderSettings, "tune", "zerolatency"); obs_data_set_string(videoEncoderSettings, "x264opts", ""); } - else if (config.Encoder == "ffmpeg_vaapi") + else if (ConfigService.Config.Encoder == "ffmpeg_vaapi") { obs_data_set_int(videoEncoderSettings, "level", 40); obs_data_set_int(videoEncoderSettings, "bitrate", 3500); @@ -440,7 +451,7 @@ void InitVideoOut() obs_data_set_int(videoEncoderSettings, "maxrate", 0); obs_data_set_bool(videoEncoderSettings, "use_bufsize", true); } - videoEncoder = obs_video_encoder_create(config.Encoder, config.Encoder + " Video Encoder", videoEncoderSettings, IntPtr.Zero); + videoEncoder = obs_video_encoder_create(ConfigService.Config.Encoder, ConfigService.Config.Encoder + " Video Encoder", videoEncoderSettings, IntPtr.Zero); obs_encoder_set_video(videoEncoder, obs_get_video()); obs_data_release(videoEncoderSettings); @@ -453,33 +464,42 @@ void InitVideoOut() var desktopAudio = obs_source_create("pulse_output_capture", "desktop_audio", IntPtr.Zero, IntPtr.Zero); obs_set_output_source(1, desktopAudio); obs_source_set_audio_mixers(desktopAudio, 1 | 2); // Adjusted mixer logic for 2 channels - obs_source_set_volume(desktopAudio, config.DesktopAudioLevel / (float)100); - + obs_source_set_volume(desktopAudio, ConfigService.Config.DesktopAudioLevel / (float)100); + obs_source_add_audio_capture_callback(desktopAudio, OnAudioData, IntPtr.Zero); var desktopEncoder = obs_audio_encoder_create("ffmpeg_aac", "desktop_audio_encoder", IntPtr.Zero, (UIntPtr)1, IntPtr.Zero); audioEncoders.Add("desktop_audio_encoder", desktopEncoder); obs_encoder_set_audio(desktopEncoder, obs_get_audio()); - if (config.MicrophoneEnabled) + ToggleMic(); + } + + public void ToggleMic() + { + if (ConfigService.Config.MicrophoneEnabled) { var micAudio = obs_source_create("pulse_input_capture", "mic_audio", IntPtr.Zero, IntPtr.Zero); obs_set_output_source(2, micAudio); // Using index 2 for the second channel obs_source_set_audio_mixers(micAudio, 1 | 4); // Adjusted mixer logic for 2 channels - obs_source_set_volume(micAudio, config.MicAudioLevel / (float)100); + obs_source_set_volume(micAudio, ConfigService.Config.MicAudioLevel / (float)100); obs_source_add_audio_capture_callback(micAudio, OnAudioData, IntPtr.Zero); var micEncoder = obs_audio_encoder_create("ffmpeg_aac", "mic_audio_encoder", IntPtr.Zero, (UIntPtr)2, IntPtr.Zero); audioEncoders.Add("mic_audio_encoder", micEncoder); obs_encoder_set_audio(micEncoder, obs_get_audio()); } + else + { + obs_set_output_source(2, NULL); + audioEncoders.Remove("mic_audio_encoder"); + } + } void InitBufferOutput() { - var config = ConfigService.GetConfig(); - - var replayDir = Path.Combine(config.VideoOutputPath, "Replays"); + var replayDir = Path.Combine(ConfigService.Config.VideoOutputPath, "Replays"); Directory.CreateDirectory(replayDir); IntPtr bufferOutputSettings = obs_data_create(); @@ -487,8 +507,8 @@ void InitBufferOutput() obs_data_set_string(bufferOutputSettings, "format", "%CCYY-%MM-%DD %hh-%mm-%ss"); obs_data_set_string(bufferOutputSettings, "extension", "mp4"); //obs_data_set_int(bufferOutputSettings, "duration_sec", 60); - obs_data_set_int(bufferOutputSettings, "max_time_sec", (uint)config.ReplayBufferSeconds); - obs_data_set_int(bufferOutputSettings, "max_size_mb", (uint)config.ReplayBufferSize); + obs_data_set_int(bufferOutputSettings, "max_time_sec", (uint)ConfigService.Config.ReplayBufferSeconds); + obs_data_set_int(bufferOutputSettings, "max_size_mb", (uint)ConfigService.Config.ReplayBufferSize); bufferOutput = obs_output_create("replay_buffer", "replay_buffer_output", bufferOutputSettings, IntPtr.Zero); obs_data_release(bufferOutputSettings); @@ -503,18 +523,17 @@ void InitBufferOutput() public void UpdateBufferSettings() { - var config = ConfigService.GetConfig(); IntPtr bufferOutputSettings = obs_data_create(); - obs_data_set_int(bufferOutputSettings, "max_time_sec", (uint)config.ReplayBufferSeconds); - obs_data_set_int(bufferOutputSettings, "max_size_mb", (uint)config.ReplayBufferSize); + obs_data_set_int(bufferOutputSettings, "max_time_sec", (uint)ConfigService.Config.ReplayBufferSeconds); + obs_data_set_int(bufferOutputSettings, "max_size_mb", (uint)ConfigService.Config.ReplayBufferSize); obs_output_update(bufferOutput, bufferOutputSettings); obs_data_release(bufferOutputSettings); } public void SetupNewRecordOutput() { - var config = ConfigService.GetConfig(); - var videoDir = config.VideoOutputPath; + + var videoDir = ConfigService.Config.VideoOutputPath; Directory.CreateDirectory(videoDir); IntPtr recordOutputSettings = obs_data_create(); @@ -633,6 +652,5 @@ public class StatusModel public class VolumePeakLevel { public float Peak { get; set; } - public int Channel { get; set; } - public object Source { get; internal set; } + public string? Source { get; set; } } \ No newline at end of file diff --git a/backend/src/obs_recorder/Program.cs b/backend/src/obs_recorder/Program.cs index ee13404..a4b7bcf 100644 --- a/backend/src/obs_recorder/Program.cs +++ b/backend/src/obs_recorder/Program.cs @@ -50,28 +50,41 @@ app.MapPost("/api/ToggleBuffer", async (bool enabled, ObsRecordingService recordingService, ConfigService configService, ILogger logger) => { - try { - var config = configService.GetConfig(); + try + { + var config = configService.GetConfig(); - if (config.ReplayBufferEnabled == enabled) return true; + if (config.ReplayBufferEnabled == enabled) return true; - config.ReplayBufferEnabled = enabled; - await configService.SaveConfig(config); + config.ReplayBufferEnabled = enabled; + await configService.SaveConfig(config); - if (config.ReplayBufferEnabled) - { - return recordingService.StartBufferOutput(); + if (config.ReplayBufferEnabled) + { + return recordingService.StartBufferOutput(); + } + else + { + recordingService.StopBufferOutput(); + return true; + } } - else + catch (Exception ex) { - recordingService.StopBufferOutput(); - return true; + logger.LogError(ex, "Failed to toggle buffer"); + return false; } +}); + +app.MapGet("/api/ToggleMic", async (ObsRecordingService recordingService, ILogger logger) => +{ + try + { + recordingService.ToggleMic(); } catch (Exception ex) { - logger.LogError(ex, "Failed to toggle buffer"); - return false; + logger.LogError(ex, "Failed to toggle mic"); } }); @@ -80,7 +93,7 @@ { try { - logger.LogInformation("Status event stream connected"); + logger.LogInformation("volume event stream connected"); var response = context.Response; response.Headers.Append("Content-Type", "text/event-stream"); @@ -91,13 +104,11 @@ Action volumeChangedAction = async (data) => { - logger.LogInformation("Status changed"); if (!cts.Token.IsCancellationRequested) { try { var serializedData = JsonSerializer.Serialize(data, VolumePeakLevelSourceGenerationContext.Default.VolumePeakLevel); - logger.LogInformation("Sending status event {data}", serializedData); await response.WriteAsync("event: peak\n"); await response.WriteAsync($"data: {serializedData}\n\n"); await response.Body.FlushAsync(); @@ -110,7 +121,6 @@ }; - recordingService.OnVolumePeakChanged += volumeChangedAction; context.RequestAborted.Register(() => @@ -131,7 +141,7 @@ }); -app.MapGet("/api/status-event", async (ILogger logger, HttpContext context, ObsRecordingService recordingService) => +app.MapGet("/api/status-event", async (ILogger logger, HttpContext context, ObsRecordingService recordingService, ConfigService configService) => { try { @@ -162,10 +172,17 @@ logger.LogError(ex, "Failed to send status event"); } } - }; + configService.ConfigChanged += async (object o, System.EventArgs sender) => + { + var serializedData = JsonSerializer.Serialize(configService.Config, ConfigSourceGenerationContext.Default.ConfigModel); + await response.WriteAsync("event: config\n"); + await response.WriteAsync($"data: {serializedData}\n\n"); + await response.Body.FlushAsync(); + + }; recordingService.OnStatusChanged += statusChangedAction; context.RequestAborted.Register(() => @@ -173,6 +190,14 @@ logger.LogInformation("Request aborted"); cts.Cancel(); recordingService.OnStatusChanged -= statusChangedAction; + configService.ConfigChanged -= async (object o, System.EventArgs sender) => + { + var serializedData = JsonSerializer.Serialize(configService.Config, ConfigSourceGenerationContext.Default.ConfigModel); + await response.WriteAsync("event: config\n"); + await response.WriteAsync($"data: {serializedData}\n\n"); + await response.Body.FlushAsync(); + + }; }); await Task.Delay(Timeout.Infinite, cts.Token); @@ -185,6 +210,8 @@ } }); + + var recorder = app.Services.GetRequiredService(); recorder.Init(); diff --git a/src/index.tsx b/src/index.tsx index 98fff36..52d5e64 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,11 +19,17 @@ import menu_icon from "../assets/sd_button_menu.svg"; import steam_icon from "../assets/sd_button_steam.svg"; interface ConfigType { - micVolume: number; - speakerVolume: number; - replayBufferEnabled: boolean, - replayBufferSeconds: number, - streamingService: string, + videoOutputPath: string; + replayBufferEnabled: boolean; + replayBufferSeconds: number; + encoder: string; + replayBufferSize: number; + micAudioLevel: number; + desktopAudioLevel: number; + microphoneEnabled: boolean; + streamingService: string; + streamingKey: string; + fps: number; } const InvokeAction = async (action: string, obj: any = null) => { @@ -72,39 +78,41 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({ serverAPI }) => { const evtSource = new EventSource("http://localhost:9988/api/status-event"); evtSource.onmessage = (event) => { - console.log(event.data); - var status = JSON.parse(event.data); - console.log(status); - setIsRecording(status.recording); - }; + console.log(event.data); + var status = JSON.parse(event.data); + console.log(status); + setIsRecording(status.recording); + }; - evtSource.addEventListener("status", (event) => { - var status = JSON.parse(event.data); - setIsRecording(status.recording); - console.log(event); - }); - + evtSource.addEventListener("status", (event) => { + var status = JSON.parse(event.data); + setIsRecording(status.recording); + console.log(event); + }); + evtSource.addEventListener("config", (event) => { + var status = JSON.parse(event.data); + SetConfig(status); + }); - const volSource = new EventSource("http://localhost:9988/api/volume-event"); - volSource.addEventListener("peak", (event) => { - console.log(event); + const volSource = new EventSource("http://localhost:9988/api/volume-event"); - var level = JSON.parse(event.data); - SetPeakVolumeMicLevel(level.peak); + volSource.addEventListener("peak", (event) => { + console.log(event); - if (level.source == "mic"){ - // SetPeakVolumeMicLevel(level.peak); - } else if (level.source == "desktop") { - SetPeakDesktopLevel(level.peak); - } - }); + var level = JSON.parse(event.data); + if (level.Source == "mic_audio") { + SetPeakVolumeMicLevel(level.Peak); + } else if (level.Source == "desktop_audio") { + SetPeakDesktopLevel(level.Peak); + } + }); - volSource.onerror = (error) => { - console.error("EventSource failed:", error); - evtSource.close(); + volSource.onerror = (error) => { + console.error("EventSource failed:", error); + evtSource.close(); }; @@ -125,14 +133,20 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({ serverAPI }) => { const ToggleBuffer = async (checked: boolean) => { var success = JSON.parse(await InvokeAction("ToggleBuffer", checked)); - if (success) { - SetConfig({ ...Config, replayBufferEnabled: checked }); - } + // if (success) { + // SetConfig({ ...Config, replayBufferEnabled: checked }); + // } + } + + const ToggleMic = async (checked: boolean) => { + SaveConfig({ ...Config, microphoneEnabled: checked }); + await InvokeAction("ToggleMic", checked); + } const SaveConfig = (Config: ConfigType) => { InvokeAction("SaveConfig", Config); - SetConfig(Config); + // SetConfig(Config); } const ChangeBufferSeconds = async (seconds: number) => { @@ -221,35 +235,40 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({ serverAPI }) => { SaveConfig({...Config, streamingService: x.data}) }/> + { data: "twitch", label: "Twitch" }, + { data: "youtube", label: "Youtube" }, + { data: "beam", label: "Beam" }, + { data: "whip", label: "WebRTC (WHIP)" }, + { data: "custom", label: "Custom" } + ]} + selectedOption="twitch" + onChange={(x) => SaveConfig({ ...Config, streamingService: x.data })} /> {isStreaming ? "Stop Streaming" : "Start Streaming"} - + - +
- + -
- + {Config.microphoneEnabled &&
+ + +
+ +
+ } ); @@ -273,33 +292,33 @@ export default definePlugin((serverApi: ServerAPI) => { icon: , critical: true, }) - }).catch(() => { - serverApi.toaster.toast({ - title: "Failed to start recording", - body: "", - icon: , - critical: true, - }) - }); - } else { - InvokeAction("StopRecording").then(() => { - serverApi.toaster.toast({ - title: "Recording saved", - // body: "Tap to view", - body: "", - icon: , - critical: true, - }) - }).catch(() => { - serverApi.toaster.toast({ - title: "Failed to save recording", - body: "", - icon: , - critical: true, + }).catch(() => { + serverApi.toaster.toast({ + title: "Failed to start recording", + body: "", + icon: , + critical: true, + }) + }); + } else { + InvokeAction("StopRecording").then(() => { + serverApi.toaster.toast({ + title: "Recording saved", + // body: "Tap to view", + body: "", + icon: , + critical: true, + }) + }).catch(() => { + serverApi.toaster.toast({ + title: "Failed to save recording", + body: "", + icon: , + critical: true, + }) }) - }) + } } - } } else if (inputs.ulButtons && inputs.ulButtons & (1 << 13) && inputs.ulButtons & (1 << 14)) { if (!isPressed) { isPressed = true;