En resumen
- Empezamos con SCRecordingOutput y lo sustituimos por nuestros propios AVAssetWriter: un archivo por fuente, todos sincronizados con un único reloj compartido.
- La burbuja de cámara, el cursor y el zoom se dibujan al exportar con un compositor AVVideoCompositing propio, así que siguen siendo editables después de grabar.
- En un M1, el codificador H.264 por hardware llega como máximo a unos 85 fps a 2880×1800: la velocidad de exportación la marca el codificador, no el dibujo.
- La mayoría de nuestros bugs más difíciles no estaban en el código de captura: permisos ligados a firmas, una pantalla bloqueada, un fallo del reproductor de SwiftUI.
ScreenCaptureKit es el framework de macOS para capturar la pantalla, y hace bien ese único trabajo: te entrega fotogramas y audio. Los archivos, la sincronización, la cámara, la exportación y los permisos te tocan a ti. Esto es lo que aprendimos al construir sobre él JustRec, un grabador nativo para la barra de menús.
La versión corta: empezamos con la salida de grabación integrada de Apple, pasamos a nuestros propios escritores, que guardan cada fuente en su propio archivo con un único reloj compartido, y ahora dibujamos lo demás al exportar. Los bugs más difíciles no tenían nada que ver con la captura.
Primera versión
SCRecordingOutput
Un flujo, un MP4 terminado. La forma más rápida de tener un prototipo que funciona.
Qué se rompió
Un archivo no bastaba
La cámara, un zoom editable y una pista de voz limpia para los subtítulos no cabían en un único archivo grabado.
Segunda versión
Escritores separados con un solo reloj
La pantalla, la cámara, el audio del sistema y el micrófono van cada uno a su propio AVAssetWriter, sincronizados por un único reloj de captura compartido.
Tercera versión
Un compositor al exportar
La burbuja, el cursor, los clics y el zoom se dibujan con Core Image cuando pulsas parar, así que siguen siendo editables.
Por qué exigimos macOS 15
JustRec funciona en macOS 15 Sequoia o posterior. No fue una decisión de marketing. En macOS 15, ScreenCaptureKit incorporó la captura del micrófono: captureMicrophone y microphoneCaptureDeviceID en SCStreamConfiguration, además de un tipo de salida .microphone en el flujo. SCRecordingOutput llegó en la misma versión.
Capturar el micrófono dentro del flujo significa que el audio del sistema y tu voz vienen de la misma fuente, con marcas de tiempo del mismo reloj. En sistemas anteriores tendrías que hacer una captura de audio aparte y alinearla tú. Dejar fuera macOS 14 nos cuesta algunos usuarios potenciales y nos ahorra toda una familia de bugs de sincronización.
let config = SCStreamConfiguration()
config.minimumFrameInterval = CMTime(value: 1, timescale: 30)
config.capturesAudio = true // audio del sistema
config.excludesCurrentProcessAudio = true // sin nuestros propios sonidos
config.captureMicrophone = true // macOS 15
config.microphoneCaptureDeviceID = microphoneID // macOS 15
Primera versión: SCRecordingOutput
Nuestro primer prototipo usaba SCRecordingOutput. Lo conectas al flujo, le das la URL de un archivo y obtienes un MP4 al detener la grabación. Para una grabación de pantalla sin más, es el camino más rápido hacia una app que funciona, y lo recomendaríamos si es todo lo que necesitas.
Dejó de encajar cuando tuvimos claro lo que debía hacer el producto:
- La cámara viene de otro sitio. La burbuja de cámara es una
AVCaptureSession(cámara integrada, webcam externa o un iPhone con Cámara de continuidad), no forma parte del flujo de pantalla. - Queríamos la burbuja fuera de la captura de pantalla. Si la ventana de la burbuja queda grabada dentro de los fotogramas de pantalla, un zoom automático también la amplía. Queríamos hacer zoom sobre la pantalla y mantener la burbuja del mismo tamaño.
- Los subtítulos necesitan una pista de voz limpia. La transcripción funciona mejor solo con el micrófono, sin los sonidos del sistema mezclados, y puede empezar antes de que exista el vídeo final.
- Los efectos tienen que seguir siendo editables. El zoom automático, el resaltado de clics y el cursor se pueden cambiar después de grabar, así que no se pueden incrustar mientras grabas.
Un único archivo terminado no podía darnos nada de eso, así que escribimos nuestros propios escritores.
| SCRecordingOutput (primera versión) | Nuestros propios escritores (ahora) | |
|---|---|---|
| Salida | Un MP4 terminado | Un archivo por fuente, más events.json |
Cámara desde una AVCaptureSession |
✗ no forma parte del flujo | ✓ en su propio archivo, con el mismo reloj |
| Zoom sin ampliar la burbuja | ✗ burbuja incrustada en los fotogramas | ✓ burbuja dibujada después del zoom |
| Pista de voz para subtítulos | ✗ espera al archivo terminado | ✓ micrófono en su propia pista |
| Efectos editables después de grabar | ✗ | ✓ se vuelven a renderizar desde los mismos archivos |
| Código que escribir y mantener | Muy poco | Escritores, reloj, compositor, exportador |
Segunda versión: escritores separados con un solo reloj
Cada fuente va ahora a su propio AVAssetWriter: screen.mp4, camera.mp4, una pista de audio del sistema y una pista de micrófono, más un events.json con la posición del ratón (muestreada 60 veces por segundo), los clics, la posición y la forma de la burbuja, y las imágenes del cursor. Todo vive en una carpeta de proyecto; el MP4 que compartes se renderiza a partir de ella.
La regla que lo hizo funcionar: cada muestra toma su tiempo de un único reloj de captura compartido, basado en el reloj de tiempo del host. El reloj también gestiona las pausas. Los fotogramas que llegan durante una pausa se descartan, y la duración de cada pausa anterior se resta de las marcas de tiempo posteriores, así que los archivos no tienen huecos.
func outputTime(forHost time: CMTime) -> CMTime? {
guard let start, time >= start else { return nil }
var output = time - start
for pause in pauses {
guard time >= pause.start else { break }
guard let end = pause.end, time >= end else { return nil } // en pausa: descartar
output = output - (end - pause.start)
}
return output
}
La cámara funciona en su propia sesión de captura con su propio reloj, así que sus marcas de tiempo se convierten a tiempo del host antes de llegar al reloj compartido:
var time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
if let source = session.synchronizationClock {
time = CMSyncConvertTime(time, from: source, to: CMClockGetHostTimeClock())
}
Algunos detalles que nos costaron tiempo:
- Los archivos empiezan tarde. La cámara puede entregar su primer fotograma medio segundo después que la pantalla. Cada archivo registra su hora de inicio real y, al exportar, se coloca con ese desfase, nunca en cero. En el vídeo, el primer fotograma se mantiene hasta que el archivo empieza de verdad, así que no hay un destello negro.
- Los formatos de audio cambian a mitad de grabación. Si cambias de micrófono mientras grabas, el formato de las muestras puede cambiar. En lugar de pasarle un formato nuevo al escritor anterior, cerramos el archivo y abrimos
mic-2.m4aen el momento actual. El exportador vuelve a poner las piezas en orden. - Escribe solo fotogramas completos. ScreenCaptureKit adjunta un estado a cada fotograma. Solo escribimos los marcados como
.completey nos saltamos el resto. - Cierra bien el último fotograma. Si no se mueve nada en pantalla, el último fotograma puede llegar mucho antes de que pulses parar. Añadimos una copia justo antes de la hora de fin, para que el vídeo dure lo mismo que la grabación.
Tercera versión: un compositor al exportar
Cuando pulsas parar, montamos una AVMutableComposition con los archivos del proyecto y la renderizamos con un compositor AVVideoCompositing propio. Para cada fotograma de salida recibe el fotograma de pantalla y el de cámara, y dibuja el resultado con Core Image: el fondo y los márgenes, los pulsos de los clics, un cursor redibujado, la cámara virtual del zoom automático y, por último, la burbuja de cámara, recortada en círculo, cuadrado o 16:9, con borde y sombra.
El audio pasa por un exportador de lectura y escritura: un AVAssetReaderAudioMixOutput mezcla el audio del sistema y el micrófono en un único flujo estéreo a 48 kHz, que se codifica en AAC junto al vídeo H.264. Como el zoom y la burbuja se dibujan aquí, cambiar un zoom más tarde solo significa volver a renderizar a partir de los mismos archivos.
La misma composición alimenta también la vista previa. Mientras la exportación sigue en marcha, el reproductor muestra la composición en directo, así que puedes ver el vídeo nada más detener la grabación.
Empezamos exportando en HEVC y cambiamos a H.264. Los archivos son algo más grandes, pero se reproducen en cualquier navegador del ordenador de cualquier cliente, y eso importa más en un enlace para compartir que unos pocos megas.
El cuello de botella es el codificador
Esperábamos que la parte lenta fuera el dibujo. No lo es. En un M1, el codificador H.264 por hardware llega como máximo a unos 85 fotogramas por segundo a 2880×1800. Una grabación de diez minutos a 30 fps son 18 000 fotogramas, así que, incluso en el mejor caso, exportar un vídeo Retina lleva unos tres minutos y medio.
La prueba de que el dibujo no es el problema: cuando añadimos el zoom automático con desenfoque de movimiento y fusión de fotogramas durante los cambios de zoom, la velocidad de exportación no cambió. Core Image en la GPU va sobrado; el ritmo lo marca el codificador.
Así que las palancas tienen que ver con el codificador y con la espera, no con el código de dibujo. La más importante para los usuarios es el selector de resolución de .
| Palanca | Efecto |
|---|---|
| Full HD en lugar de Retina | Menos píxeles por fotograma para el codificador |
| Captura a 30 fps | La mitad de fotogramas que a 60 fps |
| Mostrar la composición en directo en la vista previa | No tienes que esperar para verlo |
| Subir solo después de exportar | Un archivo, una subida, sin recodificar en el servidor |
Tropiezos que no eran de captura
El permiso de grabación de pantalla que volvía una y otra vez
macOS seguía pidiendo permiso de grabación de pantalla incluso después de haberlo concedido. La causa: una copia de depuración antigua, firmada ad hoc y con el mismo bundle id, seguía en el disco y tenía el permiso. El sistema veía dos apps con un mismo nombre y firmas distintas.
La solución fue eliminar las copias que sobraban y restablecer el permiso:
tccutil reset ScreenCapture dev.wearenube.rec
La lección va de firmas, no de ScreenCaptureKit: firma todas las compilaciones de la misma forma y no distribuyas nunca compilaciones ad hoc. Si eres usuario y estás atrapado en el mismo bucle, nuestra guía para arreglar el permiso de grabación de pantalla en el Mac explica los pasos sin necesidad de Terminal.
Un Mac bloqueado graba el fondo de pantalla
Si la pantalla se bloquea mientras grabas, el vídeo muestra solo el fondo de pantalla. No se cuelga nada ni salta ningún error: simplemente obtienes minutos de fondo de escritorio. Mantén el Mac activo y no dejes que se bloquee mientras grabas. Es el tipo de fallo que solo descubres cuando lo descubre el cliente.
El VideoPlayer de SwiftUI se colgaba
Nuestras vistas de reproducción usaban al principio el VideoPlayer de SwiftUI. Compilado con el SDK de Xcode 26, se colgaba en macOS 15. Lo cambiamos por un AVPlayerView de AppKit envuelto y los cuelgues desaparecieron:
struct PlayerView: NSViewRepresentable {
let player: AVPlayer
func makeNSView(context: Context) -> AVPlayerView {
let view = AVPlayerView()
view.player = player
view.controlsStyle = .floating
return view
}
func updateNSView(_ view: AVPlayerView, context: Context) {
if view.player !== player { view.player = player }
}
}
Quedarse sin disco
Archivos separados más una exportación significan que, durante un rato, una grabación ocupa más del doble de su tamaño final. JustRec no empieza a grabar con menos de 2 GB libres, y detiene y guarda la grabación si el espacio libre baja de 500 MB mientras grabas.
Lo que volveríamos a hacer
Si vas a construir sobre ScreenCaptureKit
- Elige tu macOS mínimo según las API que necesitas, no al revés.
- Pon todas las fuentes en un mismo reloj desde el primer día; añadir la sincronización después es un suplicio.
- Conserva las fuentes en bruto y renderiza al exportar si algo debe seguir siendo editable.
- Mide el codificador antes de optimizar tu código de dibujo.
- Firma siempre igual, también en desarrollo, y elimina las compilaciones antiguas.
- Prueba con la pantalla bloqueada, con el disco lleno y con un micrófono que cambia a mitad de grabación.
También volveríamos a empezar con SCRecordingOutput. Nos dio un prototipo funcional muy rápido y nos enseñó exactamente qué limitaciones importaban para nuestro producto.
La pista limpia del micrófono tiene una segunda vida: es de donde salen los subtítulos. Esa historia está en Whisper en el Mac: subtítulos sin la nube.
Para lo que pasa después de la subida, consulta cuánto cuesta alojar vídeos de clientes en Cloudflare R2. Para el lado de la burbuja de cámara que ve el usuario, nuestra guía para grabar la pantalla y la cámara a la vez lo muestra en la práctica, y la lista de funciones enseña en qué se traduce toda esta maquinaria.
Fuentes
- Apple Developer: ScreenCaptureKit, consultado en noviembre de 2026.
- Apple Developer: SCStreamConfiguration.captureMicrophone (macOS 15.0), consultado en noviembre de 2026.
- Apple Developer: SCStreamConfiguration.microphoneCaptureDeviceID (macOS 15.0), consultado en noviembre de 2026.
- Apple Developer: SCRecordingOutput (macOS 15.0), consultado en noviembre de 2026.
- Apple Developer: SCFrameStatus, consultado en noviembre de 2026.
- Apple Developer: AVVideoCompositing, consultado en noviembre de 2026.
- Apple Developer: AVAssetReaderAudioMixOutput, consultado en noviembre de 2026.
- Apple Developer: AVPlayerView, consultado en noviembre de 2026.
Preguntas frecuentes
¿Qué es ScreenCaptureKit?
ScreenCaptureKit es el framework de Apple para capturar el contenido de la pantalla y el audio en macOS. Eliges pantallas, ventanas o apps con un filtro de contenido, y te entrega el vídeo y el audio como sample buffers que tu app puede procesar o escribir en un archivo.
¿Qué versión de macOS necesitas para capturar el micrófono con ScreenCaptureKit?
macOS 15. Las opciones captureMicrophone y microphoneCaptureDeviceID de SCStreamConfiguration, el tipo de salida de micrófono y SCRecordingOutput llegaron todos con macOS 15.0, y por eso JustRec requiere macOS 15 Sequoia o posterior.
¿Uso SCRecordingOutput o AVAssetWriter?
SCRecordingOutput es la forma más rápida de obtener un archivo terminado. Si necesitas pistas separadas, una cámara de otra sesión de captura, gestionar las pausas o efectos que se puedan editar después de grabar, escribir tú los sample buffers con AVAssetWriter te da ese control.
¿Por qué una grabación con ScreenCaptureKit muestra solo el fondo de pantalla?
En nuestras pruebas pasaba cuando el Mac se bloqueaba durante la grabación: el vídeo mostraba el fondo de pantalla en lugar de las apps. Mantén el Mac activo, y no dejes que se bloquee, mientras grabas.
¿Por qué macOS no deja de pedir permiso de grabación de pantalla durante el desarrollo?
El permiso está ligado a la identidad y la firma de la app. Si una copia antigua con el mismo bundle id pero otra firma tiene el permiso, la nueva compilación no deja de pedirlo. Elimina las copias que sobran, restablece el permiso con tccutil y firma todas las compilaciones de la misma forma.



