| | 1 | /* |
| | 2 | * Copyright (c) 2011 Stefano Sabatini |
| | 3 | * |
| | 4 | * This file is part of FFmpeg. |
| | 5 | * |
| | 6 | * FFmpeg is free software; you can redistribute it and/or |
| | 7 | * modify it under the terms of the GNU Lesser General Public |
| | 8 | * License as published by the Free Software Foundation; either |
| | 9 | * version 2.1 of the License, or (at your option) any later version. |
| | 10 | * |
| | 11 | * FFmpeg is distributed in the hope that it will be useful, |
| | 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| | 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| | 14 | * Lesser General Public License for more details. |
| | 15 | * |
| | 16 | * You should have received a copy of the GNU Lesser General Public |
| | 17 | * License along with FFmpeg; if not, write to the Free Software |
| | 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| | 19 | */ |
| | 20 | |
| | 21 | /** |
| | 22 | * @file |
| | 23 | * libSDL output device |
| | 24 | */ |
| | 25 | |
| | 26 | #include <SDL.h> |
| | 27 | #include <SDL_thread.h> |
| | 28 | |
| | 29 | #include "libavutil/avstring.h" |
| | 30 | #include "libavutil/imgutils.h" |
| | 31 | #include "libavutil/opt.h" |
| | 32 | #include "libavutil/parseutils.h" |
| | 33 | #include "libavutil/pixdesc.h" |
| | 34 | #include "libavutil/time.h" |
| | 35 | #include "avdevice.h" |
| | 36 | |
| | 37 | typedef struct { |
| | 38 | AVClass *class; |
| | 39 | SDL_Window *window; |
| | 40 | SDL_Renderer *renderer; |
| | 41 | char *window_title; |
| | 42 | int window_width, window_height; /**< size of the window */ |
| | 43 | int window_fullscreen; |
| | 44 | int window_borderless; |
| | 45 | |
| | 46 | SDL_Texture *texture; |
| | 47 | int texture_fmt; |
| | 48 | |
| | 49 | int sdl_was_already_inited; |
| | 50 | SDL_Thread *event_thread; |
| | 51 | SDL_mutex *mutex; |
| | 52 | SDL_cond *init_cond; |
| | 53 | int init_ret; /* return code used to signal initialization errors */ |
| | 54 | int inited; |
| | 55 | int quit; |
| | 56 | } SDLContext; |
| | 57 | |
| | 58 | static const struct sdl_overlay_pix_fmt_entry { |
| | 59 | enum AVPixelFormat pix_fmt; int texture_fmt; |
| | 60 | } sdl_overlay_pix_fmt_map[] = { |
| | 61 | { AV_PIX_FMT_RGB8, SDL_PIXELFORMAT_RGB332 }, |
| | 62 | { AV_PIX_FMT_RGB444, SDL_PIXELFORMAT_RGB444 }, |
| | 63 | { AV_PIX_FMT_RGB555, SDL_PIXELFORMAT_RGB555 }, |
| | 64 | { AV_PIX_FMT_BGR555, SDL_PIXELFORMAT_BGR555 }, |
| | 65 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_ARGB4444 }, // these aren't implemented in FFmpeg |
| | 66 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_RGBA4444 }, // but keeping them here in-case they |
| | 67 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_ABGR4444 }, // are |
| | 68 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_BGRA4444 }, |
| | 69 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_ARGB1555 }, |
| | 70 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_RGBA5551 }, |
| | 71 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_ABGR1555 }, |
| | 72 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_BGRA5551 }, |
| | 73 | { AV_PIX_FMT_RGB565, SDL_PIXELFORMAT_RGB565 }, |
| | 74 | { AV_PIX_FMT_BGR565, SDL_PIXELFORMAT_BGR565 }, |
| | 75 | { AV_PIX_FMT_RGB24, SDL_PIXELFORMAT_RGB24 }, |
| | 76 | { AV_PIX_FMT_BGR24, SDL_PIXELFORMAT_BGR24 }, |
| | 77 | { AV_PIX_FMT_RGB24, SDL_PIXELFORMAT_RGB888 }, // is this right? |
| | 78 | { AV_PIX_FMT_RGB0, SDL_PIXELFORMAT_RGBX8888 }, |
| | 79 | { AV_PIX_FMT_BGR24, SDL_PIXELFORMAT_BGR888 }, // is this right? |
| | 80 | { AV_PIX_FMT_BGR0, SDL_PIXELFORMAT_BGRX8888 }, |
| | 81 | { AV_PIX_FMT_ARGB, SDL_PIXELFORMAT_ARGB8888 }, |
| | 82 | { AV_PIX_FMT_RGBA, SDL_PIXELFORMAT_RGBA8888 }, |
| | 83 | { AV_PIX_FMT_ABGR, SDL_PIXELFORMAT_ABGR8888 }, |
| | 84 | { AV_PIX_FMT_BGRA, SDL_PIXELFORMAT_BGRA8888 }, |
| | 85 | // { AV_PIX_FMT_NONE, SDL_PIXELFORMAT_ARGB2101010 }, // not implemented |
| | 86 | { AV_PIX_FMT_YUV420P, SDL_PIXELFORMAT_IYUV }, |
| | 87 | { AV_PIX_FMT_YUYV422, SDL_PIXELFORMAT_YUY2 }, |
| | 88 | { AV_PIX_FMT_UYVY422, SDL_PIXELFORMAT_UYVY }, |
| | 89 | { AV_PIX_FMT_NONE, 0 }, |
| | 90 | }; |
| | 91 | |
| | 92 | static int sdl2_write_trailer(AVFormatContext *s) |
| | 93 | { |
| | 94 | SDLContext *sdl = s->priv_data; |
| | 95 | |
| | 96 | sdl->quit = 1; |
| | 97 | |
| | 98 | if (sdl->texture) |
| | 99 | SDL_DestroyTexture(sdl->texture); |
| | 100 | sdl->texture = NULL; |
| | 101 | if (sdl->event_thread) |
| | 102 | SDL_WaitThread(sdl->event_thread, NULL); |
| | 103 | sdl->event_thread = NULL; |
| | 104 | if (sdl->mutex) |
| | 105 | SDL_DestroyMutex(sdl->mutex); |
| | 106 | sdl->mutex = NULL; |
| | 107 | if (sdl->init_cond) |
| | 108 | SDL_DestroyCond(sdl->init_cond); |
| | 109 | sdl->init_cond = NULL; |
| | 110 | |
| | 111 | if (sdl->renderer) |
| | 112 | SDL_DestroyRenderer(sdl->renderer); |
| | 113 | sdl->renderer = NULL; |
| | 114 | |
| | 115 | if (sdl->window) |
| | 116 | SDL_DestroyWindow(sdl->window); |
| | 117 | sdl->window = NULL; |
| | 118 | |
| | 119 | if (!sdl->sdl_was_already_inited) |
| | 120 | SDL_Quit(); |
| | 121 | |
| | 122 | return 0; |
| | 123 | } |
| | 124 | |
| | 125 | #define SDL_BASE_FLAGS (SDL_SWSURFACE|SDL_WINDOW_RESIZABLE) |
| | 126 | |
| | 127 | static int event_thread(void *arg) |
| | 128 | { |
| | 129 | AVFormatContext *s = arg; |
| | 130 | SDLContext *sdl = s->priv_data; |
| | 131 | int flags = SDL_BASE_FLAGS | (sdl->window_fullscreen ? SDL_WINDOW_FULLSCREEN : 0) | |
| | 132 | (sdl->window_borderless ? SDL_WINDOW_BORDERLESS : 0); |
| | 133 | AVStream *st = s->streams[0]; |
| | 134 | AVCodecContext *encctx = st->codec; |
| | 135 | |
| | 136 | /* initialization */ |
| | 137 | if (!sdl->sdl_was_already_inited){ |
| | 138 | if (SDL_Init(SDL_INIT_VIDEO) != 0) { |
| | 139 | av_log(s, AV_LOG_ERROR, "Unable to initialize SDL: %s\n", SDL_GetError()); |
| | 140 | sdl->init_ret = AVERROR(EINVAL); |
| | 141 | goto init_end; |
| | 142 | } |
| | 143 | } |
| | 144 | |
| | 145 | sdl->window_width = encctx->width; |
| | 146 | sdl->window_height = encctx->height; |
| | 147 | |
| | 148 | if (SDL_CreateWindowAndRenderer(sdl->window_width, sdl->window_height, |
| | 149 | flags, &sdl->window, &sdl->renderer) != 0){ |
| | 150 | av_log(sdl, AV_LOG_ERROR, "Couldn't create window and renderer: %s\n", SDL_GetError()); |
| | 151 | sdl->init_ret = AVERROR(EINVAL); |
| | 152 | goto init_end; |
| | 153 | } |
| | 154 | |
| | 155 | SDL_SetWindowTitle(sdl->window, sdl->window_title); |
| | 156 | |
| | 157 | sdl->texture = SDL_CreateTexture(sdl->renderer, sdl->texture_fmt, SDL_TEXTUREACCESS_STREAMING, |
| | 158 | sdl->window_width, sdl->window_height); |
| | 159 | |
| | 160 | if (!sdl->texture) { |
| | 161 | av_log(sdl, AV_LOG_ERROR, "Unable to set create mode: %s\n", SDL_GetError()); |
| | 162 | sdl->init_ret = AVERROR(EINVAL); |
| | 163 | goto init_end; |
| | 164 | } |
| | 165 | |
| | 166 | sdl->init_ret = 0; |
| | 167 | av_log(s, AV_LOG_VERBOSE, "w:%d h:%d fmt:%s -> w:%d h:%d\n", |
| | 168 | encctx->width, encctx->height, av_get_pix_fmt_name(encctx->pix_fmt), |
| | 169 | sdl->window_width, sdl->window_height); |
| | 170 | |
| | 171 | init_end: |
| | 172 | SDL_LockMutex(sdl->mutex); |
| | 173 | sdl->inited = 1; |
| | 174 | SDL_UnlockMutex(sdl->mutex); |
| | 175 | SDL_CondSignal(sdl->init_cond); |
| | 176 | |
| | 177 | if (sdl->init_ret < 0) |
| | 178 | return sdl->init_ret; |
| | 179 | |
| | 180 | /* event loop */ |
| | 181 | while (!sdl->quit) { |
| | 182 | SDL_Event event; |
| | 183 | while (SDL_PollEvent(&event)){ |
| | 184 | switch (event.type) { |
| | 185 | case SDL_KEYDOWN: |
| | 186 | switch (event.key.keysym.sym) { |
| | 187 | case SDLK_ESCAPE: |
| | 188 | case SDLK_q: |
| | 189 | sdl->quit = 1; |
| | 190 | break; |
| | 191 | } |
| | 192 | break; |
| | 193 | case SDL_QUIT: |
| | 194 | sdl->quit = 1; |
| | 195 | break; |
| | 196 | |
| | 197 | case SDL_WINDOWEVENT_SIZE_CHANGED: |
| | 198 | sdl->window_width = event.window.data1; |
| | 199 | sdl->window_height = event.window.data2; |
| | 200 | break; |
| | 201 | |
| | 202 | default: |
| | 203 | break; |
| | 204 | } |
| | 205 | } |
| | 206 | } |
| | 207 | |
| | 208 | return 0; |
| | 209 | } |
| | 210 | |
| | 211 | static int sdl2_write_header(AVFormatContext *s) |
| | 212 | { |
| | 213 | SDLContext *sdl = s->priv_data; |
| | 214 | AVStream *st = s->streams[0]; |
| | 215 | AVCodecContext *encctx = st->codec; |
| | 216 | int i, ret; |
| | 217 | |
| | 218 | if (!sdl->window_title) |
| | 219 | sdl->window_title = av_strdup(s->filename); |
| | 220 | |
| | 221 | if (SDL_WasInit(SDL_INIT_VIDEO)) { |
| | 222 | av_log(s, AV_LOG_WARNING, |
| | 223 | "SDL video subsystem was already inited, you could have multiple SDL outputs. This may cause unknown behaviour.\n"); |
| | 224 | sdl->sdl_was_already_inited = 1; |
| | 225 | } |
| | 226 | |
| | 227 | if ( s->nb_streams > 1 |
| | 228 | || encctx->codec_type != AVMEDIA_TYPE_VIDEO |
| | 229 | || encctx->codec_id != AV_CODEC_ID_RAWVIDEO) { |
| | 230 | av_log(s, AV_LOG_ERROR, "Only supports one rawvideo stream\n"); |
| | 231 | ret = AVERROR(EINVAL); |
| | 232 | goto fail; |
| | 233 | } |
| | 234 | |
| | 235 | for (i = 0; sdl_overlay_pix_fmt_map[i].pix_fmt != AV_PIX_FMT_NONE; i++) { |
| | 236 | if (sdl_overlay_pix_fmt_map[i].pix_fmt == encctx->pix_fmt) { |
| | 237 | sdl->texture_fmt = sdl_overlay_pix_fmt_map[i].texture_fmt; |
| | 238 | break; |
| | 239 | } |
| | 240 | } |
| | 241 | |
| | 242 | if (!sdl->texture_fmt) { |
| | 243 | av_log(s, AV_LOG_ERROR, |
| | 244 | "Unsupported pixel format '%s', choose one of yuv420p, yuyv422, uyvy422, BGRA\n", |
| | 245 | av_get_pix_fmt_name(encctx->pix_fmt)); |
| | 246 | ret = AVERROR(EINVAL); |
| | 247 | goto fail; |
| | 248 | } |
| | 249 | |
| | 250 | sdl->init_cond = SDL_CreateCond(); |
| | 251 | if (!sdl->init_cond) { |
| | 252 | av_log(s, AV_LOG_ERROR, "Could not create SDL condition variable: %s\n", SDL_GetError()); |
| | 253 | ret = AVERROR_EXTERNAL; |
| | 254 | goto fail; |
| | 255 | } |
| | 256 | sdl->mutex = SDL_CreateMutex(); |
| | 257 | if (!sdl->mutex) { |
| | 258 | av_log(s, AV_LOG_ERROR, "Could not create SDL mutex: %s\n", SDL_GetError()); |
| | 259 | ret = AVERROR_EXTERNAL; |
| | 260 | goto fail; |
| | 261 | } |
| | 262 | sdl->event_thread = SDL_CreateThread(event_thread, "SDL2 Event Thread",s); |
| | 263 | if (!sdl->event_thread) { |
| | 264 | av_log(s, AV_LOG_ERROR, "Could not create SDL event thread: %s\n", SDL_GetError()); |
| | 265 | ret = AVERROR_EXTERNAL; |
| | 266 | goto fail; |
| | 267 | } |
| | 268 | |
| | 269 | /* wait until the video system has been inited */ |
| | 270 | SDL_LockMutex(sdl->mutex); |
| | 271 | while (!sdl->inited) { |
| | 272 | SDL_CondWait(sdl->init_cond, sdl->mutex); |
| | 273 | } |
| | 274 | SDL_UnlockMutex(sdl->mutex); |
| | 275 | if (sdl->init_ret < 0) { |
| | 276 | av_log(s, AV_LOG_DEBUG, "sdl->init_ret < 0\n"); |
| | 277 | ret = sdl->init_ret; |
| | 278 | goto fail; |
| | 279 | } |
| | 280 | SDL_PumpEvents(); |
| | 281 | return 0; |
| | 282 | |
| | 283 | fail: |
| | 284 | sdl2_write_trailer(s); |
| | 285 | return ret; |
| | 286 | } |
| | 287 | |
| | 288 | static int sdl2_write_packet(AVFormatContext *s, AVPacket *pkt) |
| | 289 | { |
| | 290 | int ret = 0; |
| | 291 | |
| | 292 | SDLContext *sdl = s->priv_data; |
| | 293 | AVCodecContext *encctx = s->streams[0]->codec; |
| | 294 | uint8_t *data[4]; |
| | 295 | int linesize[4]; |
| | 296 | |
| | 297 | if (sdl->quit) { |
| | 298 | sdl2_write_trailer(s); |
| | 299 | return AVERROR(EIO); |
| | 300 | } |
| | 301 | |
| | 302 | av_image_fill_arrays(data, linesize, pkt->data, encctx->pix_fmt, encctx->width, encctx->height, 1); |
| | 303 | SDL_LockMutex(sdl->mutex); |
| | 304 | switch (sdl->texture_fmt) { |
| | 305 | case SDL_PIXELFORMAT_IYUV: |
| | 306 | case SDL_PIXELFORMAT_YUY2: |
| | 307 | case SDL_PIXELFORMAT_UYVY: |
| | 308 | ret = SDL_UpdateYUVTexture(sdl->texture, NULL, |
| | 309 | data[0], linesize[0], |
| | 310 | data[1], linesize[1], |
| | 311 | data[2], linesize[2]); |
| | 312 | break; |
| | 313 | case SDL_PIXELFORMAT_RGB332: |
| | 314 | case SDL_PIXELFORMAT_RGB444: |
| | 315 | case SDL_PIXELFORMAT_RGB555: |
| | 316 | case SDL_PIXELFORMAT_BGR555: |
| | 317 | // case SDL_PIXELFORMAT_ARGB4444: |
| | 318 | // case SDL_PIXELFORMAT_RGBA4444: |
| | 319 | // case SDL_PIXELFORMAT_ABGR4444: |
| | 320 | // case SDL_PIXELFORMAT_BGRA4444: |
| | 321 | // case SDL_PIXELFORMAT_ARGB1555: |
| | 322 | // case SDL_PIXELFORMAT_RGBA5551: |
| | 323 | // case SDL_PIXELFORMAT_ABGR1555: |
| | 324 | // case SDL_PIXELFORMAT_BGRA5551: |
| | 325 | case SDL_PIXELFORMAT_RGB565: |
| | 326 | case SDL_PIXELFORMAT_BGR565: |
| | 327 | case SDL_PIXELFORMAT_RGB24: |
| | 328 | case SDL_PIXELFORMAT_BGR24: |
| | 329 | case SDL_PIXELFORMAT_RGB888: // is this right? |
| | 330 | case SDL_PIXELFORMAT_RGBX8888: |
| | 331 | case SDL_PIXELFORMAT_BGR888: // is this right? |
| | 332 | case SDL_PIXELFORMAT_BGRX8888: |
| | 333 | case SDL_PIXELFORMAT_ARGB8888: |
| | 334 | case SDL_PIXELFORMAT_RGBA8888: |
| | 335 | case SDL_PIXELFORMAT_ABGR8888: |
| | 336 | case SDL_PIXELFORMAT_BGRA8888: |
| | 337 | // case SDL_PIXELFORMAT_ARGB2101010: |
| | 338 | ret = SDL_UpdateTexture(sdl->texture, NULL, data[0], linesize[0]); |
| | 339 | break; |
| | 340 | default: |
| | 341 | av_log(NULL, AV_LOG_FATAL, "Unsupported pixel format\n"); |
| | 342 | ret = -1; |
| | 343 | break; |
| | 344 | } |
| | 345 | SDL_RenderClear(sdl->renderer); |
| | 346 | SDL_RenderCopy(sdl->renderer, sdl->texture, NULL, NULL); |
| | 347 | SDL_RenderPresent(sdl->renderer); |
| | 348 | SDL_UnlockMutex(sdl->mutex); |
| | 349 | SDL_PumpEvents(); |
| | 350 | return ret; |
| | 351 | } |
| | 352 | |
| | 353 | #define OFFSET(x) offsetof(SDLContext,x) |
| | 354 | |
| | 355 | static const AVOption options[] = { |
| | 356 | { "window_title", "set SDL window title", OFFSET(window_title), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, AV_OPT_FLAG_ENCODING_PARAM }, |
| | 357 | { "window_size", "set SDL window forced size", OFFSET(window_width), AV_OPT_TYPE_IMAGE_SIZE, { .str = NULL }, 0, 0, AV_OPT_FLAG_ENCODING_PARAM }, |
| | 358 | { "window_fullscreen", "set SDL window fullscreen", OFFSET(window_fullscreen), AV_OPT_TYPE_INT, { .i64 = 0 }, INT_MIN, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM }, |
| | 359 | { "window_borderless", "set SDL window border off", OFFSET(window_fullscreen), AV_OPT_TYPE_INT, { .i64 = 0 }, INT_MIN, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM }, |
| | 360 | { NULL }, |
| | 361 | }; |
| | 362 | |
| | 363 | static const AVClass sdl2_class = { |
| | 364 | .class_name = "sdl2 outdev", |
| | 365 | .item_name = av_default_item_name, |
| | 366 | .option = options, |
| | 367 | .version = LIBAVUTIL_VERSION_INT, |
| | 368 | .category = AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT, |
| | 369 | }; |
| | 370 | |
| | 371 | AVOutputFormat ff_sdl2_muxer = { |
| | 372 | .name = "sdl2", |
| | 373 | .long_name = NULL_IF_CONFIG_SMALL("SDL2 output device"), |
| | 374 | .priv_data_size = sizeof(SDLContext), |
| | 375 | .audio_codec = AV_CODEC_ID_NONE, |
| | 376 | .video_codec = AV_CODEC_ID_RAWVIDEO, |
| | 377 | .write_header = sdl2_write_header, |
| | 378 | .write_packet = sdl2_write_packet, |
| | 379 | .write_trailer = sdl2_write_trailer, |
| | 380 | .flags = AVFMT_NOFILE | AVFMT_VARIABLE_FPS | AVFMT_NOTIMESTAMPS, |
| | 381 | .priv_class = &sdl2_class, |
| | 382 | }; |
| | 383 | No newline at end of file |