Skip to main content

shiny-screenrecorder

Generate code using Shiny.ScreenRecorder for cross-platform screen recording - capturing the screen to an MP4 with optional microphone and system audio, choosing a display or window, pausing and resuming, and handling the OS ending a recording on its own, on Android, iOS, Mac Catalyst, macOS, Windows, Linux, and Blazor WebAssembly

跳到安装

来源信息

仓库
shinyorg/shiny
最近来源活动
2026年9月5日 18:32
检测到的 SKILL.md 语言
英语
星标
1,583
分支
248

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
shiny-screenrecorder
description
Generate code using Shiny.ScreenRecorder for cross-platform screen recording - capturing the screen to an MP4 with optional microphone and system audio, choosing a display or window, pausing and resuming, and handling the OS ending a recording on its own, on Android, iOS, Mac Catalyst, macOS, Windows, Linux, and Blazor WebAssembly
auto_invoke
true
triggers
["screen record","screen recorder","screen recording","record the screen","record my screen","screen capture","capture the screen","screencast","screen cast","record app video","record a video of the app","capture video of the screen","session replay video","bug report video","record a demo","record a tutorial","screen share","share my screen","IScreenRecorder","IScreenRecording","ScreenRecordingRequest","ScreenRecordingResult","ScreenRecorderCapabilities","ScreenRecordingFaultReason","CaptureTarget","ScreenRecorderNotSupportedException","ScreenRecorderPermissionException","AddScreenRecorder","Shiny.ScreenRecorder","ReplayKit","RPScreenRecorder","startCapture","[Truncated]"]
# Shiny.ScreenRecorder Skill You are an expert in Shiny.ScreenRecorder, a cross-platform screen recording library covering **video capture to a file**, **microphone and system audio**, **display/window selection**, **pause and resume**, and **the OS ending a recording without being asked**. ## When to Use This Skill Invoke this skill when the user wants to: - Record the screen (or their app's screen) to a video file - Attach a screen recording to a bug report or support ticket - Record a demo, tutorial or session replay from inside an app - Capture the microphone, the app's own audio, or the machine's audio alongside video - Let the user pick which display or window to record - Pause and resume a recording - Handle the user or the OS stopping a recording partway through - Ask why screen recording "only records my own app on iPhone" ## ⚠️ Read this before writing anything **What "the screen" means is not the same on every platform, and the difference is visible to users.** Three facts decide most of the design: 1. **iOS and Mac Catalyst record your own app's UI only.** ReplayKit's in-app path is all a NuGet package can offer - system-wide capture needs a Broadcast Upload Extension, which is a second app target the consumer must create. Never tell a user their iPhone app can record other apps. 2. **Windows has no audio at all.** `Windows.Graphics.Capture` captures pixels and nothing else. Asking for `IncludeMicrophone` or `IncludeSystemAudio` there **throws**. 3. **Capabilities differ within a platform, not just between them.** macOS 15 gains microphone capture and *loses* pause; macOS 12.3-14 is the other way round. Read `recorder.Capabilities` off the instance at runtime. Never infer it from the target framework. A request asking for something outside `Capabilities` throws `ScreenRecorderNotSupportedException` **before any native call happens** - by design, because a recording that silently came out without the microphone is worse than one that refused to start. **Branch on the flags.** ## Capability matrix | | Android | iOS / Mac Catalyst | tvOS | macOS 15+ | macOS 12.3-14 | Windows | Linux | Blazor WASM | |---|---|---|---|---|---|---|---|---| | What is recorded | system screen | **this app only** | **this app only** | system screen | system screen | system screen | system screen | user's pick | | Record | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Pause / Resume | ✅ | ✅ synth | ✅ synth | ❌ | ✅ synth | ✅ synth | ❌ | ✅ **native** | | Microphone | ✅ | ✅ | ❌ **no mic** | ✅ | ❌ | ❌ | ✅ | ✅ | | System audio | ✅ API 29+ (app audio) | ✅ (app audio) | ✅ (app audio) | ✅ | ✅ | ❌ | ✅ | ⚠️ Chromium, tab only | | Pick a display | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | portal picker | browser picker | | Pick a window | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | portal picker | browser picker | | Hide the cursor | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | | Frame rate | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | | Bitrate | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Downscale | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Result has a file path | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ **null** | **tvOS** is the same ReplayKit implementation as iOS. The only difference is the microphone: an Apple TV has none, `RPScreenRecorder` carries no `MicrophoneEnabled` on tvOS, so `ScreenRecorderCapabilities.Microphone` is not advertised and `IncludeMicrophone = true` is rejected by request validation. Never generate a tvOS recording request that sets it. ### The four things users most often ask for that cannot be done 1. **Recording other apps from an iPhone app.** ReplayKit's in-app capture is scoped to your own process. The only route to the system screen is a Broadcast Upload Extension target, which this package does not ship. Do not suggest a workaround; there isn't one. 2. **Audio on Windows.** `Windows.Graphics.Capture` has no audio path. Adding it means a hand-written WASAPI loopback capture, which this library does not do. `Capabilities` reports neither audio flag on Windows. 3. **Hiding the Android cast indicator or the Windows 11 capture border.** Both are OS-drawn recording indicators and neither is suppressible from a normal app. This is deliberate on the platforms' part and the library does not try to work around it. 4. **Silently starting a recording.** Every platform except iOS-recording-itself puts a consent step in front of it - Android's dialog, macOS's TCC grant, the Linux portal picker, the browser picker. There is no unattended screen recording here. ## Library Overview | Item | Value | |-----------|-------| | GitHub | https://github.com/shinyorg/shiny | | NuGet | `Shiny.ScreenRecorder`, plus `Shiny.ScreenRecorder.Linux` on Linux and `Shiny.ScreenRecorder.Blazor` in the browser | | Namespace | `Shiny.ScreenRecorder` (types); `Shiny` (registration extensions) | | Platforms | Android, iOS, Mac Catalyst, macOS, Windows, Linux, Blazor WebAssembly | ### How each platform is backed | Platform | Capture | Encoder | |---|---|---| | Android | `MediaProjection` → `VirtualDisplay` | `MediaCodec` (H.264 surface + AAC) → `MediaMuxer` | | iOS / Mac Catalyst | `RPScreenRecorder.startCapture` | `AVAssetWriter` | | macOS 15+ | `SCStream` | `SCRecordingOutput` (ScreenCaptureKit writes the file) | | macOS 12.3-14 | `SCStream` + `ISCStreamOutput` | `AVAssetWriter` | | Windows | `Direct3D11CaptureFramePool` | `MediaStreamSource` → `MediaTranscoder` | | Linux | xdg-desktop-portal `ScreenCast` → PipeWire | `gst-launch-1.0`, or `ffmpeg -f x11grab` | | Blazor WASM | `getDisplayMedia` | `MediaRecorder` | | plain .NET | none - every call throws | none | **Android uses MediaCodec rather than the much simpler MediaRecorder for one reason:** `MediaRecorder.setAudioSource` takes a single source and playback capture is not one of them, so app audio is only reachable through `AudioRecord` + `AudioPlaybackCaptureConfiguration`. Wanting app audio at all forces the whole pipeline down. ## Registration ```csharp builder.Services.AddScreenRecorder(); // IScreenRecorder, singleton ``` Same call on every platform. On Linux reference `Shiny.ScreenRecorder.Linux` and in a Blazor WebAssembly app reference `Shiny.ScreenRecorder.Blazor` **instead of** the base package - each registers its own implementation of the same interface. On a plain .NET host - a server, console or test project with no screen - the base package offers `AddNotSupportedScreenRecorder()` instead, which registers a recorder reporting `ScreenRecorderCapabilities.None`. It is named differently on purpose: the Linux and Blazor packages register a *real* implementation under `AddScreenRecorder` on that same target framework, so sharing the name would make every call ambiguous in a project referencing one of them. ### Platform setup **Android** - `AndroidManifest.xml`: ```xml <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- only if capturing audio --> ``` The foreground service and the consent activity are in the package and merge into your manifest automatically. From Android 14 the service **must** be running before the projection is obtained; the library does that ordering for you. **iOS / Mac Catalyst** - no entitlement to record your own app. Add `NSMicrophoneUsageDescription` to `Info.plist` if using the microphone. The app must be in the **foreground**. **macOS** - the Screen Recording grant in System Settings. `RequestAccess` prompts for it, but **macOS only applies a new grant on the next launch** - the first `RequestAccess` after the user approves still reports `Denied`, and you must tell them to restart the app. Add `NSMicrophoneUsageDescription`, and `com.apple.security.device.audio-input` when sandboxed. **Windows** - Windows 10 1903 or later. Packaged apps declare the `graphicsCapture` capability. **Linux** - a desktop session with `xdg-desktop-portal` implementing ScreenCast, plus `gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-pipewire`, or `ffmpeg` on X11. Audio needs `pactl`. Flatpak sandboxes are **not** supported. All of this is probed at runtime - a machine missing the pieces reports `ScreenRecorderCapabilities.None`. **Blazor WebAssembly** - HTTPS (or localhost) and a **user gesture**. `getDisplayMedia` is refused from `OnInitializedAsync`; it must run from a button click. In an iframe, add `allow="display-capture; microphone"`. ## The basic recording ```csharp public class BugReportRecorder(IScreenRecorder recorder, ILogger<BugReportRecorder> logger) { IScreenRecording? session; public async Task Start(CancellationToken ct) { var request = new ScreenRecordingRequest { // null lets the library name a file in the platform cache directory OutputPath = null, IncludeMicrophone = recorder.Capabilities.HasFlag(ScreenRecorderCapabilities.Microphone), MaxWidth = 1280, // a phone screen at native resolution is enormous MaxDuration = TimeSpan.FromMinutes(2) }; var access = await recorder.RequestAccess(request, ct); if (access is AccessState.Denied or AccessState.NotSupported) throw new InvalidOperationException("Screen recording is not available"); // does not return until frames are genuinely being written - the consent dialog, the // compositor picker and the Android foreground service all complete first this.session = await recorder.Start(request, ct); this.session.Faulted += (_, e) => logger.LogWarning("The recording ended on its own: {Reason}", e.Reason); } public async Task<ScreenRecordingResult> Stop(CancellationToken ct) { var result = await this.session!.Stop(ct); this.session = null; return result; } } ``` **`MaxWidth` is worth setting on almost every recording.** A modern phone or Retina display at native resolution produces very large files for very little visible gain. **`RequestAccess` cannot always answer.** Android's consent dialog is bound to the projection it authorises and cannot be pre-granted, and the Linux portal and browser pickers grant per call - all three report `AccessState.Unknown`. Treat anything other than `Denied` / `NotSupported` as "worth trying", and let `Start` surface the real answer. ## Reading the result ```csharp var result = await session.Stop(ct); logger.LogInformation( "{Duration} of {Width}x{Height} {MimeType}, {Bytes} bytes", result.Duration, result.Width, result.Height, result.MimeType, result.ByteSize ); // portable - works on every platform including the browser await using var stream = await result.OpenRead(ct); await UploadAsync(stream, result.MimeType, ct); ``` **`FilePath` is null in the browser** - there is no filesystem. Use `OpenRead()` when you want the bytes and do not care where they came from. **`MimeType` genuinely varies**: native platforms all produce `video/mp4`, but Firefox produces `video/webm;codecs=vp9`. Do not hardcode `.mp4` when uploading or naming a download. On Android the file is in app-private cache - move or share it before the OS reclaims it. On Apple platforms it is inside the app container and is **not** in Photos until you put it there. ## Pausing ```csharp if (recorder.Capabilities.HasFlag(ScreenRecorderCapabilities.PauseResume)) { await session.Pause(ct); // ... user does something private ... await session.Resume(ct); } ``` Both calls are idempotent. Only the browser pauses natively; elsewhere the capture keeps running, frames are dropped, and later timestamps are shifted back so the output has **no frozen stretch** - which also means a long pause still costs battery. `Elapsed` excludes the paused span and matches the duration of the finished file. ## Choosing a display or window ```csharp if (recorder.Capabilities.HasFlag(ScreenRecorderCapabilities.DisplaySelection)) { var targets = await recorder.GetTargets(ct); var display = targets.FirstOrDefault(t => t.Kind == CaptureTargetKind.Display && t.IsPrimary) ?? targets.First(); await recorder.Start(new ScreenRecordingRequest { Target = display }, ct); } else { // mobile has no concept of a target; Linux and the browser show their own picker during Start await recorder.Start(new ScreenRecordingRequest(), ct); } ``` `CaptureTarget.Id` is the platform's own handle and is **not stable** across reboots or across a window being closed and reopened. Re-enumerate rather than persisting one. Window titles are often empty or duplicated - pair `Name` with `ApplicationName` when showing a list. **`GetTargets` throws on Linux and in the browser.** Their compositors insist on running their own picker, which appears during `Start`. `Target` must be left null there. ## When the OS ends it for you This is not an edge case - it is the normal way a screen recording ends on several platforms. ```csharp session.Faulted += (_, e) => { // by now the session is finished; Stop() will return what was salvaged rather than continuing switch (e.Reason) { case ScreenRecordingFaultReason.RevokedByUser: // Android's cast notification, the browser's "Stop sharing" bar, macOS's menu-bar stop break; case ScreenRecordingFaultReason.InterruptedBySystem: // an incoming call on iOS, an Android foreground-service timeout, the screen locking break; case ScreenRecordingFaultReason.MaxDurationReached: // stopped cleanly - e.Result always carries a complete file break; case ScreenRecordingFaultReason.TargetLost: // a monitor unplugged, a recorded window closed break; case ScreenRecordingFaultReason.EncoderFailed: // e.Result is usually null and the file is unusable break; } if (e.Result != null) Save(e.Result); }; ``` `Faulted` fires on a **native callback thread**. Marshal before touching UI. The same is true of `IScreenRecorder.StateChanged`. ## Lifecycle rules - **One recording at a time.** `Start` throws `ScreenRecorderException` while another is in flight - every platform underneath has the same restriction. - **Stop or dispose. Never just drop it.** Disposing without `Stop` cancels and **deletes the partial file**. - `Stop` twice returns the same result. `Stop` after `Cancel` throws - there is no output. - Stopping is not instant. Flushing the encoder and writing the container index takes a moment on a long recording, and killing the process during it leaves a file with no index that will not play. ## Blazor specifics ```razor <button @onclick="StartRecording">Record</button> @* must be a user gesture *@ @code { [Inject] IScreenRecorder Recorder { get; set; } = null!; IScreenRecording? session; protected override async Task OnInitializedAsync() { // Capabilities is synchronous but browser feature detection is not, so it reports None // until this has run await ((BlazorScreenRecorder)this.Recorder).Probe(); } async Task StartRecording() => this.session = await this.Recorder.Start(new ScreenRecordingRequest()); async Task StopRecording() { var result = await this.session!.Stop(); // the browser is the only place the recording is not already a file the app can move var extension = result.MimeType.Contains("mp4") ? "mp4" : "webm"; await this.Recorder.DownloadRecording(result, $"recording.{extension}"); } } ``` ## Common mistakes | Mistake | What happens | Do instead | |---|---|---| | Assuming iOS records the whole screen | Only your app is in the file | Say so in the UI; use Android/desktop for full-screen capture | | Asking for audio on Windows | `ScreenRecorderNotSupportedException` | Check `Capabilities` first | | Reading `Capabilities` off the TFM | Wrong on macOS, where 15+ and 12.3-14 differ | Read it off the instance at runtime | | Recording at native resolution on a phone | Enormous files | Set `MaxWidth` | | Calling `Start` from `OnInitializedAsync` in Blazor | The browser refuses it | Call it from a button click | | Hardcoding `.mp4` in the browser | Firefox produces WebM | Read `result.MimeType` | | Ignoring `Faulted` | A recording the user stopped looks like a hang | Subscribe before the first frame | | Dropping the session without stopping | The file is deleted | `Stop()`, or `Cancel()` if you mean to discard it | | Persisting a `CaptureTarget.Id` | Stale handle on the next run | Re-enumerate with `GetTargets` | | Expecting `RequestAccess` to settle Android consent | It reports `Unknown` | Let `Start` show the dialog | ## Exceptions | Exception | Meaning | |---|---| | `ScreenRecorderNotSupportedException` | The platform cannot do it at all. Retrying never helps; the message names the limit. Check the matching capability flag first. | | `ScreenRecorderPermissionException` | Fixable - a missing manifest entry, entitlement or usage description, or the user declined a consent dialog. | | `ScreenRecorderException` | Base type, and everything else: a recording already in flight, an encoder failure, a file that never got written. |
在 GitHub 查看