History

lavdevice: Add AudioToolbox output device.

Thilo Borgmann committed 6 years ago

Commit b7375751 parent 9d80f3e6 files · +360 −0Browse files

Unified · Split

6 files
  1. Changelog +1 −0
  2. configure +3 −0
  3. doc/outdevs.texi +46 −0
  4. libavdevice/Makefile +1 −0
  5. libavdevice/alldevices.c +1 −0
  6. libavdevice/audiotoolbox.m +308 −0
Changelog+1−0
@@ -2,6 +2,7 @@ Entries are sorted chronologically from oldest to youngest within each release, · 1 unmodified line
2releases are sorted from youngest to oldest.2releases are sorted from youngest to oldest.
3 3
4version <next>:4version <next>:
5- AudioToolbox output device
5 6
6 7
7version 4.3:8version 4.3:
configure+3−0
@@ -3367,6 +3367,8 @@ alsa_outdev_deps="alsa" · 3366 unmodified lines
3367avfoundation_indev_deps="avfoundation corevideo coremedia pthreads"3367avfoundation_indev_deps="avfoundation corevideo coremedia pthreads"
3368avfoundation_indev_suggest="coregraphics applicationservices"3368avfoundation_indev_suggest="coregraphics applicationservices"
3369avfoundation_indev_extralibs="-framework Foundation"3369avfoundation_indev_extralibs="-framework Foundation"
3370audiotoolbox_outdev_deps="audiotoolbox pthreads"
3371audiotoolbox_outdev_extralibs="-framework AudioToolbox -framework CoreAudio"
3370bktr_indev_deps_any="dev_bktr_ioctl_bt848_h machine_ioctl_bt848_h dev_video_bktr_ioctl_bt848_h dev_ic_bt8xx_h"3372bktr_indev_deps_any="dev_bktr_ioctl_bt848_h machine_ioctl_bt848_h dev_video_bktr_ioctl_bt848_h dev_ic_bt8xx_h"
3371caca_outdev_deps="libcaca"3373caca_outdev_deps="libcaca"
3372decklink_deps_any="libdl LoadLibrary"3374decklink_deps_any="libdl LoadLibrary"
@@ -6152,6 +6154,7 @@ enabled videotoolbox && check_apple_framework VideoToolbox · 2779 unmodified lines
6152check_apple_framework CoreFoundation6154check_apple_framework CoreFoundation
6153check_apple_framework CoreMedia6155check_apple_framework CoreMedia
6154check_apple_framework CoreVideo6156check_apple_framework CoreVideo
6157check_apple_framework CoreAudio
6155 6158
6156enabled avfoundation && {6159enabled avfoundation && {
6157 disable coregraphics applicationservices6160 disable coregraphics applicationservices
doc/outdevs.texi+46−0
@@ -38,6 +38,52 @@ ffmpeg -i INPUT -f alsa hw:1,7 · 37 unmodified lines
38@end example38@end example
39@end itemize39@end itemize
40 40
41@section AudioToolbox
42
43AudioToolbox output device.
44
45Allows native output to CoreAudio devices on OSX.
46
47The output filename can be empty (or @code{-}) to refer to the default system output device or a number that refers to the device index as shown using: @code{-list_devices true}.
48
49Alternatively, the audio input device can be chosen by index using the
50@option{
51 -audio_device_index <INDEX>
52}
53, overriding any device name or index given in the input filename.
54
55All available devices can be enumerated by using @option{-list_devices true}, listing
56all device names, UIDs and corresponding indices.
57
58@subsection Options
59
60AudioToolbox supports the following options:
61
62@table @option
63
64@item -audio_device_index <INDEX>
65Specify the audio device by its index. Overrides anything given in the output filename.
66
67@end table
68
69@subsection Examples
70
71@itemize
72
73@item
74Print the list of supported devices and output a sine wave to the default device:
75@example
76$ ffmpeg -f lavfi -i sine=r=44100 -f audiotoolbox -list_devices true -
77@end example
78
79@item
80Output a sine wave to the device with the index 2, overriding any output filename:
81@example
82$ ffmpeg -f lavfi -i sine=r=44100 -f audiotoolbox -audio_device_index 2 -
83@end example
84
85@end itemize
86
41@section caca87@section caca
42 88
43CACA output device.89CACA output device.
libavdevice/Makefile+1−0
@@ -15,6 +15,7 @@ OBJS-$(CONFIG_SHARED) += reverse.o · 14 unmodified lines
15OBJS-$(CONFIG_ALSA_INDEV) += alsa_dec.o alsa.o timefilter.o15OBJS-$(CONFIG_ALSA_INDEV) += alsa_dec.o alsa.o timefilter.o
16OBJS-$(CONFIG_ALSA_OUTDEV) += alsa_enc.o alsa.o16OBJS-$(CONFIG_ALSA_OUTDEV) += alsa_enc.o alsa.o
17OBJS-$(CONFIG_ANDROID_CAMERA_INDEV) += android_camera.o17OBJS-$(CONFIG_ANDROID_CAMERA_INDEV) += android_camera.o
18OBJS-$(CONFIG_AUDIOTOOLBOX_OUTDEV) += audiotoolbox.o
18OBJS-$(CONFIG_AVFOUNDATION_INDEV) += avfoundation.o19OBJS-$(CONFIG_AVFOUNDATION_INDEV) += avfoundation.o
19OBJS-$(CONFIG_BKTR_INDEV) += bktr.o20OBJS-$(CONFIG_BKTR_INDEV) += bktr.o
20OBJS-$(CONFIG_CACA_OUTDEV) += caca.o21OBJS-$(CONFIG_CACA_OUTDEV) += caca.o
libavdevice/alldevices.c+1−0
@@ -27,6 +27,7 @@ · 26 unmodified lines
27extern AVInputFormat ff_alsa_demuxer;27extern AVInputFormat ff_alsa_demuxer;
28extern AVOutputFormat ff_alsa_muxer;28extern AVOutputFormat ff_alsa_muxer;
29extern AVInputFormat ff_android_camera_demuxer;29extern AVInputFormat ff_android_camera_demuxer;
30extern AVOutputFormat ff_audiotoolbox_muxer;
30extern AVInputFormat ff_avfoundation_demuxer;31extern AVInputFormat ff_avfoundation_demuxer;
31extern AVInputFormat ff_bktr_demuxer;32extern AVInputFormat ff_bktr_demuxer;
32extern AVOutputFormat ff_caca_muxer;33extern AVOutputFormat ff_caca_muxer;
libavdevice/audiotoolbox.m+308−0
@@ -0,0 +1,308 @@
1/*
2 * AudioToolbox output device
3 * Copyright (c) 2020 Thilo Borgmann <thilo.borgmann@mail.de>
4 *
5 * This file is part of FFmpeg.
6 *
7 * FFmpeg is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
11 *
12 * FFmpeg is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with FFmpeg; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 */
21
22/**
23 * @file
24 * AudioToolbox output device
25 * @author Thilo Borgmann <thilo.borgmann@mail.de>
26 */
27
28#import <AudioToolbox/AudioToolbox.h>
29#include <pthread.h>
30
31#include "libavutil/opt.h"
32#include "libavformat/internal.h"
33#include "libavutil/internal.h"
34#include "avdevice.h"
35
36typedef struct
37{
38 AVClass *class;
39
40 AudioQueueBufferRef buffer[2];
41 pthread_mutex_t buffer_lock[2];
42 int cur_buf;
43 AudioQueueRef queue;
44
45 int list_devices;
46 int audio_device_index;
47
48} ATContext;
49
50static int check_status(AVFormatContext *avctx, OSStatus *status, const char *msg)
51{
52 if (*status != noErr) {
53 av_log(avctx, AV_LOG_ERROR, "Error: %s (%i)\n", msg, *status);
54 return 1;
55 } else {
56 av_log(avctx, AV_LOG_DEBUG, " OK : %s\n", msg);
57 return 0;
58 }
59}
60
61static void queue_callback(void* atctx, AudioQueueRef inAQ,
62 AudioQueueBufferRef inBuffer)
63{
64 // unlock the buffer that has just been consumed
65 ATContext *ctx = (ATContext*)atctx;
66 for (int i = 0; i < 2; i++) {
67 if (inBuffer == ctx->buffer[i]) {
68 pthread_mutex_unlock(&ctx->buffer_lock[i]);
69 }
70 }
71}
72
73static av_cold int at_write_header(AVFormatContext *avctx)
74{
75 ATContext *ctx = (ATContext*)avctx->priv_data;
76 OSStatus err = noErr;
77 CFStringRef device_UID = NULL;
78 AudioDeviceID *devices;
79 int num_devices;
80
81
82 // get devices
83 UInt32 data_size = 0;
84 AudioObjectPropertyAddress prop;
85 prop.mSelector = kAudioHardwarePropertyDevices;
86 prop.mScope = kAudioObjectPropertyScopeGlobal;
87 prop.mElement = kAudioObjectPropertyElementMaster;
88 err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, NULL, &data_size);
89 if (check_status(avctx, &err, "AudioObjectGetPropertyDataSize devices"))
90 return AVERROR(EINVAL);
91
92 num_devices = data_size / sizeof(AudioDeviceID);
93
94 devices = (AudioDeviceID*)(av_malloc(data_size));
95 err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, NULL, &data_size, devices);
96 if (check_status(avctx, &err, "AudioObjectGetPropertyData devices")) {
97 av_freep(&devices);
98 return AVERROR(EINVAL);
99 }
100
101 // list devices
102 if (ctx->list_devices) {
103 CFStringRef device_name = NULL;
104 prop.mScope = kAudioDevicePropertyScopeInput;
105
106 av_log(ctx, AV_LOG_INFO, "CoreAudio devices:\n");
107 for(UInt32 i = 0; i < num_devices; ++i) {
108 // UID
109 data_size = sizeof(device_UID);
110 prop.mSelector = kAudioDevicePropertyDeviceUID;
111 err = AudioObjectGetPropertyData(devices[i], &prop, 0, NULL, &data_size, &device_UID);
112 if (check_status(avctx, &err, "AudioObjectGetPropertyData UID"))
113 continue;
114
115 // name
116 data_size = sizeof(device_name);
117 prop.mSelector = kAudioDevicePropertyDeviceNameCFString;
118 err = AudioObjectGetPropertyData(devices[i], &prop, 0, NULL, &data_size, &device_name);
119 if (check_status(avctx, &err, "AudioObjecTGetPropertyData name"))
120 continue;
121
122 av_log(ctx, AV_LOG_INFO, "[%d] %30s, %s\n", i,
123 CFStringGetCStringPtr(device_name, kCFStringEncodingMacRoman),
124 CFStringGetCStringPtr(device_UID, kCFStringEncodingMacRoman));
125 }
126 }
127
128 // get user-defined device UID or use default device
129 // -audio_device_index overrides any URL given
130 const char *stream_name = avctx->url;
131 if (stream_name && ctx->audio_device_index == -1) {
132 sscanf(stream_name, "%d", &ctx->audio_device_index);
133 }
134
135 if (ctx->audio_device_index >= 0) {
136 // get UID of selected device
137 data_size = sizeof(device_UID);
138 prop.mSelector = kAudioDevicePropertyDeviceUID;
139 err = AudioObjectGetPropertyData(devices[ctx->audio_device_index], &prop, 0, NULL, &data_size, &device_UID);
140 if (check_status(avctx, &err, "AudioObjecTGetPropertyData UID")) {
141 av_freep(&devices);
142 return AVERROR(EINVAL);
143 }
144 } else {
145 // use default device
146 device_UID = NULL;
147 }
148
149 av_log(ctx, AV_LOG_DEBUG, "stream_name: %s\n", stream_name);
150 av_log(ctx, AV_LOG_DEBUG, "audio_device_idnex: %i\n", ctx->audio_device_index);
151 av_log(ctx, AV_LOG_DEBUG, "UID: %s\n", CFStringGetCStringPtr(device_UID, kCFStringEncodingMacRoman));
152
153 // check input stream
154 if (avctx->nb_streams != 1 || avctx->streams[0]->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) {
155 av_log(ctx, AV_LOG_ERROR, "Only a single audio stream is supported.\n");
156 return AVERROR(EINVAL);
157 }
158
159 av_freep(&devices);
160 AVCodecParameters *codecpar = avctx->streams[0]->codecpar;
161
162 // audio format
163 AudioStreamBasicDescription device_format = {0};
164 device_format.mSampleRate = codecpar->sample_rate;
165 device_format.mFormatID = kAudioFormatLinearPCM;
166 device_format.mFormatFlags |= (codecpar->format == AV_SAMPLE_FMT_FLT) ? kLinearPCMFormatFlagIsFloat : 0;
167 device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S8) ? kLinearPCMFormatFlagIsSignedInteger : 0;
168 device_format.mFormatFlags |= (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE)) ? kLinearPCMFormatFlagIsSignedInteger : 0;
169 device_format.mFormatFlags |= (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S24BE, AV_CODEC_ID_PCM_S24LE)) ? kLinearPCMFormatFlagIsSignedInteger : 0;
170 device_format.mFormatFlags |= (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S32BE, AV_CODEC_ID_PCM_S32LE)) ? kLinearPCMFormatFlagIsSignedInteger : 0;
171 device_format.mFormatFlags |= (av_sample_fmt_is_planar(codecpar->format)) ? kAudioFormatFlagIsNonInterleaved : 0;
172 device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_F32BE) ? kAudioFormatFlagIsBigEndian : 0;
173 device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S16BE) ? kAudioFormatFlagIsBigEndian : 0;
174 device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S24BE) ? kAudioFormatFlagIsBigEndian : 0;
175 device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S32BE) ? kAudioFormatFlagIsBigEndian : 0;
176 device_format.mChannelsPerFrame = codecpar->channels;
177 device_format.mBitsPerChannel = (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S24BE, AV_CODEC_ID_PCM_S24LE)) ? 24 : (av_get_bytes_per_sample(codecpar->format) << 3);
178 device_format.mBytesPerFrame = (device_format.mBitsPerChannel >> 3) * device_format.mChannelsPerFrame;
179 device_format.mFramesPerPacket = 1;
180 device_format.mBytesPerPacket = device_format.mBytesPerFrame * device_format.mFramesPerPacket;
181 device_format.mReserved = 0;
182
183 av_log(ctx, AV_LOG_DEBUG, "device_format.mSampleRate = %i\n", codecpar->sample_rate);
184 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatID = %s\n", "kAudioFormatLinearPCM");
185 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->format == AV_SAMPLE_FMT_FLT) ? "kLinearPCMFormatFlagIsFloat" : "0");
186 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S8) ? "kLinearPCMFormatFlagIsSignedInteger" : "0");
187 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S32BE, AV_CODEC_ID_PCM_S32LE)) ? "kLinearPCMFormatFlagIsSignedInteger" : "0");
188 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE)) ? "kLinearPCMFormatFlagIsSignedInteger" : "0");
189 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S24BE, AV_CODEC_ID_PCM_S24LE)) ? "kLinearPCMFormatFlagIsSignedInteger" : "0");
190 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (av_sample_fmt_is_planar(codecpar->format)) ? "kAudioFormatFlagIsNonInterleaved" : "0");
191 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_F32BE) ? "kAudioFormatFlagIsBigEndian" : "0");
192 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S16BE) ? "kAudioFormatFlagIsBigEndian" : "0");
193 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S24BE) ? "kAudioFormatFlagIsBigEndian" : "0");
194 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S32BE) ? "kAudioFormatFlagIsBigEndian" : "0");
195 av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags == %i\n", device_format.mFormatFlags);
196 av_log(ctx, AV_LOG_DEBUG, "device_format.mChannelsPerFrame = %i\n", codecpar->channels);
197 av_log(ctx, AV_LOG_DEBUG, "device_format.mBitsPerChannel = %i\n", av_get_bytes_per_sample(codecpar->format) << 3);
198 av_log(ctx, AV_LOG_DEBUG, "device_format.mBytesPerFrame = %i\n", (device_format.mBitsPerChannel >> 3) * codecpar->channels);
199 av_log(ctx, AV_LOG_DEBUG, "device_format.mBytesPerPacket = %i\n", device_format.mBytesPerFrame);
200 av_log(ctx, AV_LOG_DEBUG, "device_format.mFramesPerPacket = %i\n", 1);
201 av_log(ctx, AV_LOG_DEBUG, "device_format.mReserved = %i\n", 0);
202
203 // create new output queue for the device
204 err = AudioQueueNewOutput(&device_format, queue_callback, ctx,
205 NULL, kCFRunLoopCommonModes,
206 0, &ctx->queue);
207 if (check_status(avctx, &err, "AudioQueueNewOutput")) {
208 if (err == kAudioFormatUnsupportedDataFormatError)
209 av_log(ctx, AV_LOG_ERROR, "Unsupported output format.\n");
210 return AVERROR(EINVAL);
211 }
212
213 // set user-defined device or leave untouched for default
214 if (device_UID != NULL) {
215 err = AudioQueueSetProperty(ctx->queue, kAudioQueueProperty_CurrentDevice, &device_UID, sizeof(device_UID));
216 if (check_status(avctx, &err, "AudioQueueSetProperty output UID"))
217 return AVERROR(EINVAL);
218 }
219
220 // start the queue
221 err = AudioQueueStart(ctx->queue, NULL);
222 if (check_status(avctx, &err, "AudioQueueStart"))
223 return AVERROR(EINVAL);
224
225 // init the mutexes for double-buffering
226 pthread_mutex_init(&ctx->buffer_lock[0], NULL);
227 pthread_mutex_init(&ctx->buffer_lock[1], NULL);
228
229 return 0;
230}
231
232static int at_write_packet(AVFormatContext *avctx, AVPacket *pkt)
233{
234 ATContext *ctx = (ATContext*)avctx->priv_data;
235 OSStatus err = noErr;
236
237 // use the other buffer
238 ctx->cur_buf = !ctx->cur_buf;
239
240 // lock for writing or wait for the buffer to be available
241 // will be unlocked by queue callback
242 pthread_mutex_lock(&ctx->buffer_lock[ctx->cur_buf]);
243
244 // (re-)allocate the buffer if not existant or of different size
245 if (!ctx->buffer[ctx->cur_buf] || ctx->buffer[ctx->cur_buf]->mAudioDataBytesCapacity != pkt->size) {
246 err = AudioQueueAllocateBuffer(ctx->queue, pkt->size, &ctx->buffer[ctx->cur_buf]);
247 if (check_status(avctx, &err, "AudioQueueAllocateBuffer")) {
248 pthread_mutex_unlock(&ctx->buffer_lock[ctx->cur_buf]);
249 return AVERROR(ENOMEM);
250 }
251 }
252
253 AudioQueueBufferRef buf = ctx->buffer[ctx->cur_buf];
254
255 // copy audio data into buffer and enqueue the buffer
256 memcpy(buf->mAudioData, pkt->data, buf->mAudioDataBytesCapacity);
257 buf->mAudioDataByteSize = buf->mAudioDataBytesCapacity;
258 err = AudioQueueEnqueueBuffer(ctx->queue, buf, 0, NULL);
259 if (check_status(avctx, &err, "AudioQueueEnqueueBuffer")) {
260 pthread_mutex_unlock(&ctx->buffer_lock[ctx->cur_buf]);
261 return AVERROR(EINVAL);
262 }
263
264 return 0;
265}
266
267static av_cold int at_write_trailer(AVFormatContext *avctx)
268{
269 ATContext *ctx = (ATContext*)avctx->priv_data;
270 OSStatus err = noErr;
271
272 pthread_mutex_destroy(&ctx->buffer_lock[0]);
273 pthread_mutex_destroy(&ctx->buffer_lock[1]);
274
275 err = AudioQueueFlush(ctx->queue);
276 check_status(avctx, &err, "AudioQueueFlush");
277 err = AudioQueueDispose(ctx->queue, true);
278 check_status(avctx, &err, "AudioQueueDispose");
279
280 return 0;
281}
282
283static const AVOption options[] = {
284 { "list_devices", "list available audio devices", offsetof(ATContext, list_devices), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, AV_OPT_FLAG_ENCODING_PARAM },
285 { "audio_device_index", "select audio device by index (starts at 0)", offsetof(ATContext, audio_device_index), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM },
286 { NULL },
287};
288
289static const AVClass at_class = {
290 .class_name = "AudioToolbox",
291 .item_name = av_default_item_name,
292 .option = options,
293 .version = LIBAVUTIL_VERSION_INT,
294 .category = AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,
295};
296
297AVOutputFormat ff_audiotoolbox_muxer = {
298 .name = "audiotoolbox",
299 .long_name = NULL_IF_CONFIG_SMALL("AudioToolbox output device"),
300 .priv_data_size = sizeof(ATContext),
301 .audio_codec = AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE),
302 .video_codec = AV_CODEC_ID_NONE,
303 .write_header = at_write_header,
304 .write_packet = at_write_packet,
305 .write_trailer = at_write_trailer,
306 .flags = AVFMT_NOFILE,
307 .priv_class = &at_class,
308};

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t