From 5801152b037d1dda801f9ed49621cfd373f8c323 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 6 Sep 2022 23:04:55 +0200 Subject: [PATCH] Add `/video` to automatically give HLS to supported browser --- cmd/camera-streamer/http.c | 4 +++- device/buffer_lock.c | 7 +++++- device/buffer_lock.h | 2 +- html/index.html | 12 ++++++---- output/http_ffmpeg.c | 1 + output/http_h264.c | 2 +- output/http_hls.c | 46 ++++++++++++++++++++++++++++++++++++++ output/http_jpeg.c | 4 ++-- output/output.h | 4 ++++ util/http/http.c | 9 ++++++-- util/http/http.h | 2 ++ util/http/http_methods.c | 12 ++++++++++ 12 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 output/http_hls.c diff --git a/cmd/camera-streamer/http.c b/cmd/camera-streamer/http.c index fb89911..458f51e 100644 --- a/cmd/camera-streamer/http.c +++ b/cmd/camera-streamer/http.c @@ -76,7 +76,9 @@ http_method_t http_methods[] = { { "GET /stream?", http_stream }, { "GET /?action=snapshot", http_snapshot }, { "GET /?action=stream", http_stream }, - { "GET /video?", http_content, "text/html", html_video_html, 0, &html_video_html_len }, + { "GET /video?", http_detect_video }, + { "GET /video.html?", http_content, "text/html", html_video_html, 0, &html_video_html_len }, + { "GET /video.m3u8?", http_m3u8_video }, { "GET /video.h264?", http_h264_video }, { "GET /video.mkv?", http_mkv_video }, { "GET /video.mp4?", http_mp4_video }, diff --git a/device/buffer_lock.c b/device/buffer_lock.c index 73138e5..3b707f0 100644 --- a/device/buffer_lock.c +++ b/device/buffer_lock.c @@ -108,15 +108,20 @@ ret: return buf; } -int buffer_lock_write_loop(buffer_lock_t *buf_lock, int nframes, buffer_write_fn fn, void *data) +int buffer_lock_write_loop(buffer_lock_t *buf_lock, int nframes, unsigned timeout_ms, buffer_write_fn fn, void *data) { int counter = 0; int frames = 0; uint64_t deadline_ms = get_monotonic_time_us(NULL, NULL) + DEFAULT_BUFFER_LOCK_GET_TIMEOUT * 1000LL; + uint64_t frame_stop_ms = get_monotonic_time_us(NULL, NULL) + timeout_ms * 1000LL; buffer_lock_use(buf_lock, 1); while (nframes == 0 || frames < nframes) { + if (timeout_ms && frame_stop_ms < get_monotonic_time_us(NULL, NULL)) { + break; + } + buffer_t *buf = buffer_lock_get(buf_lock, 0, &counter); if (!buf) { goto error; diff --git a/device/buffer_lock.h b/device/buffer_lock.h index 33b44a4..77e0d72 100644 --- a/device/buffer_lock.h +++ b/device/buffer_lock.h @@ -52,6 +52,6 @@ buffer_t *buffer_lock_get(buffer_lock_t *buf_lock, int timeout_ms, int *counter) bool buffer_lock_needs_buffer(buffer_lock_t *buf_lock); void buffer_lock_use(buffer_lock_t *buf_lock, int ref); bool buffer_lock_is_used(buffer_lock_t *buf_lock); -int buffer_lock_write_loop(buffer_lock_t *buf_lock, int nframes, buffer_write_fn fn, void *data); +int buffer_lock_write_loop(buffer_lock_t *buf_lock, int nframes, unsigned timeout_ms, buffer_write_fn fn, void *data); bool buffer_lock_register_check_streaming(buffer_lock_t *buf_lock, buffer_lock_check_streaming check_streaming); bool buffer_lock_register_notify_buffer(buffer_lock_t *buf_lock, buffer_lock_notify_buffer notify_buffer); diff --git a/html/index.html b/html/index.html index c099335..9bc5d18 100644 --- a/html/index.html +++ b/html/index.html @@ -40,14 +40,18 @@
  • /video
    - Get a live WebRTC (H264) stream.
    + Get a live (H264) video stream best suited to current browser in a maximum compatibility mode choosing automatically between one of the below formats.


  • diff --git a/output/http_ffmpeg.c b/output/http_ffmpeg.c index cffb7b4..c66b256 100644 --- a/output/http_ffmpeg.c +++ b/output/http_ffmpeg.c @@ -137,6 +137,7 @@ static void http_ffmpeg_video(http_worker_t *worker, FILE *stream, const char *c int n = buffer_lock_write_loop( http_h264_buffer_for_res(worker), 0, + 0, (buffer_write_fn)http_ffmpeg_video_buf_part, &status); ffmpeg_remuxer_close(&remuxer); diff --git a/output/http_h264.c b/output/http_h264.c index e9bb57e..a91adb4 100644 --- a/output/http_h264.c +++ b/output/http_h264.c @@ -81,7 +81,7 @@ void http_h264_video(http_worker_t *worker, FILE *stream) { http_video_status_t status = { stream }; - int n = buffer_lock_write_loop(http_h264_buffer_for_res(worker), 0, (buffer_write_fn)http_video_buf_part, &status); + int n = buffer_lock_write_loop(http_h264_buffer_for_res(worker), 0, 0, (buffer_write_fn)http_video_buf_part, &status); if (status.wrote_header) { return; diff --git a/output/http_hls.c b/output/http_hls.c new file mode 100644 index 0000000..b61239f --- /dev/null +++ b/output/http_hls.c @@ -0,0 +1,46 @@ +#include +#include + +#include "output.h" +#include "util/opts/log.h" +#include "util/http/http.h" +#include "device/buffer.h" +#include "device/buffer_lock.h" +#include "device/buffer_list.h" +#include "device/device.h" + +static const char *const CONTENT_TYPE = "application/x-mpegURL"; + +static const char *const STREAM_M3U8 = + "#EXTM3U\r\n" \ + "#EXT-X-TARGETDURATION:1\r\n" \ + "#EXT-X-VERSION:4\r\n" \ + "#EXTINF:1.0,\r\n" \ + "video.mp4?ts=%zu\r\n"; + +static const char *const LOCATION_REDIRECT = + "HTTP/1.0 307 Temporary Redirect\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "Location: %s\r\n" + "\r\n"; + +void http_m3u8_video(struct http_worker_s *worker, FILE *stream) +{ + uint64_t ts = get_monotonic_time_us(NULL, NULL) / 1000 / 1000; + http_write_responsef(stream, "200 OK", CONTENT_TYPE, STREAM_M3U8, ts); +} + +void http_detect_video(struct http_worker_s *worker, FILE *stream) +{ + if (strstr(worker->user_agent, "Safari/") && !strstr(worker->user_agent, "Chrome/") && !strstr(worker->user_agent, "Chromium/")) { + // Safari only supports m3u8 + fprintf(stream, LOCATION_REDIRECT, "video.m3u8"); + } else if (strstr(worker->user_agent, "Firefox/")) { + // Firefox only supports mp4 + fprintf(stream, LOCATION_REDIRECT, "video.mp4"); + } else { + // Chrome offers best latency with mkv + fprintf(stream, LOCATION_REDIRECT, "video.mkv"); + } +} diff --git a/output/http_jpeg.c b/output/http_jpeg.c index e20fbdd..a16632c 100644 --- a/output/http_jpeg.c +++ b/output/http_jpeg.c @@ -40,7 +40,7 @@ int http_snapshot_buf_part(buffer_lock_t *buf_lock, buffer_t *buf, int frame, FI void http_snapshot(http_worker_t *worker, FILE *stream) { - int n = buffer_lock_write_loop(http_jpeg_buffer_for_res(worker), 1, (buffer_write_fn)http_snapshot_buf_part, stream); + int n = buffer_lock_write_loop(http_jpeg_buffer_for_res(worker), 1, 0, (buffer_write_fn)http_snapshot_buf_part, stream); if (n <= 0) { http_500(stream, NULL); @@ -68,7 +68,7 @@ int http_stream_buf_part(buffer_lock_t *buf_lock, buffer_t *buf, int frame, FILE void http_stream(http_worker_t *worker, FILE *stream) { - int n = buffer_lock_write_loop(http_jpeg_buffer_for_res(worker), 0, (buffer_write_fn)http_stream_buf_part, stream); + int n = buffer_lock_write_loop(http_jpeg_buffer_for_res(worker), 0, 0, (buffer_write_fn)http_stream_buf_part, stream); if (n == 0) { http_500(stream, NULL); diff --git a/output/output.h b/output/output.h index f336ee6..42ae185 100644 --- a/output/output.h +++ b/output/output.h @@ -23,4 +23,8 @@ void http_mkv_video(struct http_worker_s *worker, FILE *stream); void http_mp4_video(struct http_worker_s *worker, FILE *stream); void http_mov_video(struct http_worker_s *worker, FILE *stream); +// HLS +void http_m3u8_video(struct http_worker_s *worker, FILE *stream); +void http_detect_video(struct http_worker_s *worker, FILE *stream); + #define HTTP_LOW_RES_PARAM "res=low" diff --git a/util/http/http.c b/util/http/http.c index 7eb3bb6..b84d901 100644 --- a/util/http/http.c +++ b/util/http/http.c @@ -16,6 +16,7 @@ #define HEADER_RANGE "Range:" #define HEADER_CONTENT_LENGTH "Content-Length:" +#define HEADER_USER_AGENT "User-Agent:" static int http_listen(int port, int maxcons) { @@ -95,6 +96,7 @@ static void http_process(http_worker_t *worker, FILE *stream) } worker->range_header[0] = 0; + worker->user_agent[0] = 0; worker->content_length = -1; // Consume headers @@ -107,9 +109,10 @@ static void http_process(http_worker_t *worker, FILE *stream) if (strcasestr(line, HEADER_RANGE) == line) { strcpy(worker->range_header, line); - } - if (strcasestr(line, HEADER_CONTENT_LENGTH) == line) { + } else if (strcasestr(line, HEADER_CONTENT_LENGTH) == line) { worker->content_length = atoi(line + strlen(HEADER_CONTENT_LENGTH)); + } else if (strcasestr(line, HEADER_USER_AGENT) == line) { + strcpy(worker->user_agent, line + strlen(HEADER_USER_AGENT)); } } @@ -129,6 +132,8 @@ static void http_process(http_worker_t *worker, FILE *stream) } } + LOG_INFO(worker, "Request '%s'", worker->client_method); + if (worker->current_method) { worker->current_method->func(worker, stream); worker->current_method = NULL; diff --git a/util/http/http.h b/util/http/http.h index ffc697d..f4cb056 100644 --- a/util/http/http.h +++ b/util/http/http.h @@ -36,6 +36,7 @@ typedef struct http_worker_s { char *client_host; char client_method[BUFSIZE]; char range_header[BUFSIZE]; + char user_agent[BUFSIZE]; http_method_t *current_method; } http_worker_t; @@ -48,6 +49,7 @@ typedef struct http_server_options_s { int http_server(http_server_options_t *options, http_method_t *methods); void http_content(http_worker_t *worker, FILE *stream); void http_write_response(FILE *stream, const char *status, const char *content_type, const char *body, unsigned content_length); +void http_write_responsef(FILE *stream, const char *status, const char *content_type, const char *fmt, ...); void http_200(FILE *stream, const char *data); void http_400(FILE *stream, const char *data); void http_404(FILE *stream, const char *data); diff --git a/util/http/http_methods.c b/util/http/http_methods.c index 8c1cb68..498a8f9 100644 --- a/util/http/http_methods.c +++ b/util/http/http_methods.c @@ -1,5 +1,6 @@ #include #include +#include #include "http.h" @@ -23,6 +24,17 @@ void http_write_response( } } +void http_write_responsef(FILE *stream, const char *status, const char *content_type, const char *fmt, ...) +{ + va_list arg; + va_start(arg, fmt); + + char *body = NULL; + size_t n = vasprintf(&body, fmt, arg); + http_write_response(stream, status, content_type, body, n); + free(body); +} + void http_content(http_worker_t *worker, FILE *stream) { if (worker->current_method) {