Skip to content

Commit d7673fb

Browse files
Merge pull request #1469 from CapSoftware/windows-m4s
Implement fragmented m4s support for Windows
2 parents b61c012 + 8dfac56 commit d7673fb

File tree

10 files changed

+798
-75
lines changed

10 files changed

+798
-75
lines changed

apps/desktop/src/routes/(window-chrome)/(main).tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
Suspense,
1616
} from "solid-js";
1717
import { createStore, reconcile } from "solid-js/store";
18-
1918
import Mode from "~/components/Mode";
2019
import Tooltip from "~/components/Tooltip";
2120
import { identifyUser, trackEvent } from "~/utils/analytics";
@@ -37,6 +36,9 @@ import {
3736
type RecordingMode,
3837
type ScreenCaptureTarget,
3938
} from "~/utils/tauri";
39+
import IconCapLogoFull from "~icons/cap/logo-full";
40+
import IconCapLogoFullDark from "~icons/cap/logo-full-dark";
41+
import IconLucideBug from "~icons/lucide/bug";
4042

4143
function getWindowSize() {
4244
return {

apps/desktop/src/routes/(window-chrome)/settings/feedback.tsx

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -130,53 +130,70 @@ export default function FeedbackTab() {
130130
</p>
131131
}
132132
>
133-
{(diag) => (
134-
<div class="space-y-3 text-sm">
135-
<Show when={diag().macosVersion}>
136-
{(ver) => (
137-
<div class="space-y-1">
138-
<p class="text-gray-11 font-medium">Operating System</p>
139-
<p class="text-gray-10 bg-gray-2 px-2 py-1.5 rounded font-mono text-xs">
140-
{ver().displayName}
141-
</p>
142-
</div>
143-
)}
144-
</Show>
145-
146-
<div class="space-y-1">
147-
<p class="text-gray-11 font-medium">Capture Support</p>
148-
<div class="flex gap-2 flex-wrap">
149-
<span
150-
class={`px-2 py-1 rounded text-xs ${
151-
diag().screenCaptureSupported
152-
? "bg-green-500/20 text-green-400"
153-
: "bg-red-500/20 text-red-400"
154-
}`}
155-
>
156-
Screen Capture:{" "}
157-
{diag().screenCaptureSupported
158-
? "Supported"
159-
: "Not Supported"}
160-
</span>
161-
</div>
162-
</div>
133+
{(diag) => {
134+
const d = diag();
135+
const osVersion =
136+
"macosVersion" in d
137+
? d.macosVersion
138+
: "windowsVersion" in d
139+
? d.windowsVersion
140+
: null;
141+
const captureSupported =
142+
"screenCaptureSupported" in d
143+
? d.screenCaptureSupported
144+
: "graphicsCaptureSupported" in d
145+
? d.graphicsCaptureSupported
146+
: false;
147+
return (
148+
<div class="space-y-3 text-sm">
149+
<Show when={osVersion}>
150+
{(ver) => (
151+
<div class="space-y-1">
152+
<p class="text-gray-11 font-medium">
153+
Operating System
154+
</p>
155+
<p class="text-gray-10 bg-gray-2 px-2 py-1.5 rounded font-mono text-xs">
156+
{(ver() as { displayName: string }).displayName}
157+
</p>
158+
</div>
159+
)}
160+
</Show>
163161

164-
<Show when={diag().availableEncoders.length > 0}>
165162
<div class="space-y-1">
166-
<p class="text-gray-11 font-medium">Available Encoders</p>
167-
<div class="flex gap-1.5 flex-wrap">
168-
<For each={diag().availableEncoders}>
169-
{(encoder) => (
170-
<span class="px-2 py-1 bg-gray-2 rounded text-xs text-gray-10 font-mono">
171-
{encoder}
172-
</span>
173-
)}
174-
</For>
163+
<p class="text-gray-11 font-medium">Capture Support</p>
164+
<div class="flex gap-2 flex-wrap">
165+
<span
166+
class={`px-2 py-1 rounded text-xs ${
167+
captureSupported
168+
? "bg-green-500/20 text-green-400"
169+
: "bg-red-500/20 text-red-400"
170+
}`}
171+
>
172+
Screen Capture:{" "}
173+
{captureSupported ? "Supported" : "Not Supported"}
174+
</span>
175175
</div>
176176
</div>
177-
</Show>
178-
</div>
179-
)}
177+
178+
<Show when={d.availableEncoders.length > 0}>
179+
<div class="space-y-1">
180+
<p class="text-gray-11 font-medium">
181+
Available Encoders
182+
</p>
183+
<div class="flex gap-1.5 flex-wrap">
184+
<For each={d.availableEncoders}>
185+
{(encoder) => (
186+
<span class="px-2 py-1 bg-gray-2 rounded text-xs text-gray-10 font-mono">
187+
{encoder}
188+
</span>
189+
)}
190+
</For>
191+
</div>
192+
</div>
193+
</Show>
194+
</div>
195+
);
196+
}}
180197
</Show>
181198
</div>
182199
</div>

apps/desktop/src/routes/(window-chrome)/settings/integrations/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button } from "@cap/ui-solid";
22
import { useNavigate } from "@solidjs/router";
33
import { For, onMount } from "solid-js";
4+
import IconLucideDatabase from "~icons/lucide/database";
45

56
import "@total-typescript/ts-reset/filter-boolean";
67
import { authStore } from "~/store";

apps/desktop/src/utils/tauri.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ uploadProgressEvent: "upload-progress-event"
364364

365365
/** user-defined types **/
366366

367+
export type AllGpusInfo = { gpus: GpuInfoDiag[]; primaryGpuIndex: number | null; isMultiGpuSystem: boolean; hasDiscreteGpu: boolean }
367368
export type Annotation = { id: string; type: AnnotationType; x: number; y: number; width: number; height: number; strokeColor: string; strokeWidth: number; fillColor: string; opacity: number; rotation: number; text: string | null; maskType?: MaskType | null; maskLevel?: number | null }
368369
export type AnnotationType = "arrow" | "circle" | "rectangle" | "text" | "mask"
369370
export type AppTheme = "system" | "light" | "dark"
@@ -431,6 +432,7 @@ quality: number | null;
431432
* Whether to prioritize speed over quality (default: false)
432433
*/
433434
fast: boolean | null }
435+
export type GpuInfoDiag = { vendor: string; description: string; dedicatedVideoMemoryMb: number; adapterIndex: number; isSoftwareAdapter: boolean; isBasicRenderDriver: boolean; supportsHardwareEncoding: boolean }
434436
export type HapticPattern = "alignment" | "levelChange" | "generic"
435437
export type HapticPerformanceTime = "default" | "now" | "drawCompleted"
436438
export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean }
@@ -443,7 +445,6 @@ export type JsonValue<T> = [T]
443445
export type LogicalBounds = { position: LogicalPosition; size: LogicalSize }
444446
export type LogicalPosition = { x: number; y: number }
445447
export type LogicalSize = { width: number; height: number }
446-
export type MacOSVersionInfo = { displayName: string }
447448
export type MainWindowRecordingStartBehaviour = "close" | "minimise"
448449
export type MaskKeyframes = { position?: MaskVectorKeyframe[]; size?: MaskVectorKeyframe[]; intensity?: MaskScalarKeyframe[] }
449450
export type MaskKind = "sensitive" | "highlight"
@@ -486,6 +487,7 @@ export type RecordingStatus = "pending" | "recording"
486487
export type RecordingStopped = null
487488
export type RecordingTargetMode = "display" | "window" | "area"
488489
export type RenderFrameEvent = { frame_number: number; fps: number; resolution_base: XY<number> }
490+
export type RenderingStatus = { isUsingSoftwareRendering: boolean; isUsingBasicRenderDriver: boolean; hardwareEncodingAvailable: boolean; warningMessage: string | null }
489491
export type RequestOpenRecordingPicker = { target_mode: RecordingTargetMode | null }
490492
export type RequestOpenSettings = { page: string }
491493
export type RequestScreenCapturePrewarm = { force?: boolean }
@@ -506,7 +508,7 @@ export type StartRecordingInputs = { capture_target: ScreenCaptureTarget; captur
506508
export type StereoMode = "stereo" | "monoL" | "monoR"
507509
export type StudioRecordingMeta = { segment: SingleSegment } | { inner: MultipleSegments }
508510
export type StudioRecordingStatus = { status: "InProgress" } | { status: "NeedsRemux" } | { status: "Failed"; error: string } | { status: "Complete" }
509-
export type SystemDiagnostics = { macosVersion: MacOSVersionInfo | null; availableEncoders: string[]; screenCaptureSupported: boolean }
511+
export type SystemDiagnostics = { windowsVersion: WindowsVersionInfo | null; gpuInfo: GpuInfoDiag | null; allGpus: AllGpusInfo | null; renderingStatus: RenderingStatus; availableEncoders: string[]; graphicsCaptureSupported: boolean; d3D11VideoProcessorAvailable: boolean }
510512
export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null }
511513
export type TextSegment = { start: number; end: number; enabled?: boolean; content?: string; center?: XY<number>; size?: XY<number>; fontFamily?: string; fontSize?: number; fontWeight?: number; italic?: boolean; color?: string; fadeDuration?: number }
512514
export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[]; maskSegments?: MaskSegment[]; textSegments?: TextSegment[] }
@@ -523,6 +525,7 @@ export type VideoUploadInfo = { id: string; link: string; config: S3UploadMeta }
523525
export type WindowExclusion = { bundleIdentifier?: string | null; ownerName?: string | null; windowTitle?: string | null }
524526
export type WindowId = string
525527
export type WindowUnderCursor = { id: WindowId; app_name: string; bounds: LogicalBounds }
528+
export type WindowsVersionInfo = { major: number; minor: number; build: number; displayName: string; meetsRequirements: boolean; isWindows11: boolean }
526529
export type XY<T> = { x: T; y: T }
527530
export type ZoomMode = "auto" | { manual: { x: number; y: number } }
528531
export type ZoomSegment = { start: number; end: number; amount: number; mode: ZoomMode }

crates/enc-ffmpeg/src/mux/segmented_stream.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ impl SegmentedVideoEncoder {
148148

149149
let mut output = format::output_as(&manifest_path, "dash")?;
150150

151+
let init_seg_path = base_path.join(INIT_SEGMENT_NAME);
152+
let media_seg_pattern = base_path.join("segment_$Number%03d$.m4s");
153+
154+
#[cfg(windows)]
155+
let init_seg_str = init_seg_path.to_string_lossy().replace('\\', "/");
156+
#[cfg(windows)]
157+
let media_seg_str = media_seg_pattern.to_string_lossy().replace('\\', "/");
158+
159+
#[cfg(not(windows))]
160+
let init_seg_str = init_seg_path.to_string_lossy().to_string();
161+
#[cfg(not(windows))]
162+
let media_seg_str = media_seg_pattern.to_string_lossy().to_string();
163+
151164
unsafe {
152165
let opts = output.as_mut_ptr();
153166

@@ -157,8 +170,8 @@ impl SegmentedVideoEncoder {
157170
ffmpeg::ffi::av_opt_set((*opts).priv_data, k.as_ptr(), v.as_ptr(), 0);
158171
};
159172

160-
set_opt("init_seg_name", INIT_SEGMENT_NAME);
161-
set_opt("media_seg_name", "segment_$Number%03d$.m4s");
173+
set_opt("init_seg_name", &init_seg_str);
174+
set_opt("media_seg_name", &media_seg_str);
162175
set_opt(
163176
"seg_duration",
164177
&config.segment_duration.as_secs_f64().to_string(),

crates/recording/src/capture_pipeline.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ use crate::{
88

99
#[cfg(target_os = "macos")]
1010
use crate::output_pipeline::{MacOSFragmentedM4SMuxer, MacOSFragmentedM4SMuxerConfig};
11+
#[cfg(windows)]
12+
use crate::output_pipeline::{WindowsFragmentedM4SMuxer, WindowsFragmentedM4SMuxerConfig};
1113
use anyhow::anyhow;
14+
#[cfg(windows)]
15+
use cap_enc_ffmpeg::h264::H264Preset;
1216
use cap_timestamp::Timestamps;
1317
use std::{path::PathBuf, sync::Arc};
1418

@@ -136,11 +140,9 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
136140
output_path: PathBuf,
137141
start_time: Timestamps,
138142
fragmented: bool,
139-
_shared_pause_state: Option<SharedPauseState>,
143+
shared_pause_state: Option<SharedPauseState>,
140144
encoder_preferences: EncoderPreferences,
141145
) -> anyhow::Result<OutputPipeline> {
142-
let d3d_device = screen_capture.d3d_device.clone();
143-
144146
if fragmented {
145147
let fragments_dir = output_path
146148
.parent()
@@ -150,17 +152,15 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture {
150152
OutputPipeline::builder(fragments_dir)
151153
.with_video::<screen_capture::VideoSource>(screen_capture)
152154
.with_timestamps(start_time)
153-
.build::<WindowsSegmentedMuxer>(WindowsSegmentedMuxerConfig {
154-
pixel_format: screen_capture::Direct3DCapture::PIXEL_FORMAT.as_dxgi(),
155-
d3d_device,
156-
bitrate_multiplier: 0.15f32,
157-
frame_rate: 30u32,
158-
output_size: None,
159-
encoder_preferences,
155+
.build::<WindowsFragmentedM4SMuxer>(WindowsFragmentedM4SMuxerConfig {
160156
segment_duration: std::time::Duration::from_secs(3),
157+
preset: H264Preset::Ultrafast,
158+
output_size: None,
159+
shared_pause_state,
161160
})
162161
.await
163162
} else {
163+
let d3d_device = screen_capture.d3d_device.clone();
164164
OutputPipeline::builder(output_path.clone())
165165
.with_video::<screen_capture::VideoSource>(screen_capture)
166166
.with_timestamps(start_time)

crates/recording/src/output_pipeline/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ pub use win_segmented::*;
2929
mod win_segmented_camera;
3030
#[cfg(windows)]
3131
pub use win_segmented_camera::*;
32+
33+
#[cfg(windows)]
34+
mod win_fragmented_m4s;
35+
#[cfg(windows)]
36+
pub use win_fragmented_m4s::*;

0 commit comments

Comments
 (0)