From d9cb6885ab741ba69a966109cb05e26692143ce0 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 2 Oct 2020 16:27:59 +0200 Subject: Matrix: add video/regular file upload --- src/FileAnalyzer.cpp | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/FileAnalyzer.cpp (limited to 'src/FileAnalyzer.cpp') diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp new file mode 100644 index 0000000..690e40e --- /dev/null +++ b/src/FileAnalyzer.cpp @@ -0,0 +1,227 @@ +#include "../include/FileAnalyzer.hpp" +#include "../include/Program.h" +#include +#include +#include +#include // TODO: Remove this dependency + +static const int MAGIC_NUMBER_BUFFER_SIZE = 36; + +namespace QuickMedia { + struct MagicNumber { + std::array data; + size_t size; + ContentType content_type; + }; + + // Sources: + // https://en.wikipedia.org/wiki/List_of_file_signatures + // https://mimesniff.spec.whatwg.org/ + + // What about audio ogg files that are not opus? + // TODO: Test all of these + static const std::array magic_numbers = { + MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'A', 'V', 'I', ' '}, 12, ContentType::VIDEO_AVI }, + MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, 12, ContentType::VIDEO_MP4 }, + MagicNumber{ {0x1A, 0x45, 0xDF, 0xA3}, 4, ContentType::VIDEO_WEBM }, + MagicNumber{ {'.', 's', 'n', 'd'}, 4, ContentType::AUDIO_BASIC }, + MagicNumber{ {'F', 'O', 'R', 'M', -1, -1, -1, -1, 'A', 'I', 'F', 'F'}, 12, ContentType::AUDIO_AIFF }, + MagicNumber{ { 'I', 'D', '3' }, 3, ContentType::AUDIO_MPEG }, + MagicNumber{ { 0xFF, 0xFB }, 2, ContentType::AUDIO_MPEG }, + MagicNumber{ { 0xFF, 0xF3 }, 2, ContentType::AUDIO_MPEG }, + MagicNumber{ { 0xFF, 0xF2 }, 2, ContentType::AUDIO_MPEG }, + //MagicNumber{ {'O', 'g', 'g', 'S', 0x00}, 5 }, + MagicNumber{ {'M', 'T', 'h', 'd', -1, -1, -1, -1}, 8, ContentType::AUDIO_MIDI }, + MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'W', 'A', 'V', 'E'}, 12, ContentType::AUDIO_WAVE }, + MagicNumber{ {'f', 'L', 'a', 'C'}, 4, ContentType::AUDIO_FLAC }, + MagicNumber{ {'O', 'g', 'g', 'S', 0x00, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ,-1, -1, -1, -1, -1, -1, -1, 'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}, 36, ContentType::AUDIO_OPUS }, + MagicNumber{ {0xFF, 0xD8, 0xFF}, 3, ContentType::IMAGE_JPEG }, + MagicNumber{ {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, 8, ContentType::IMAGE_PNG }, + MagicNumber{ {'G', 'I', 'F', '8', '7', 'a'}, 6, ContentType::IMAGE_GIF }, + MagicNumber{ {'G', 'I', 'F', '8', '9', 'a'}, 6, ContentType::IMAGE_GIF }, + MagicNumber{ {'B', 'M'}, 2, ContentType::IMAGE_BMP }, + MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'W', 'E', 'B', 'V', 'P'}, 6, ContentType::IMAGE_WEBP } + }; + + bool is_content_type_video(ContentType content_type) { + return content_type >= ContentType::VIDEO_AVI && content_type <= ContentType::VIDEO_WEBM; + } + + bool is_content_type_audio(ContentType content_type) { + return content_type >= ContentType::AUDIO_BASIC && content_type <= ContentType::AUDIO_OPUS; + } + + bool is_content_type_image(ContentType content_type) { + return content_type >= ContentType::IMAGE_JPEG && content_type <= ContentType::IMAGE_WEBP; + } + + const char* content_type_to_string(ContentType content_type) { + switch(content_type) { + case ContentType::UNKNOWN: return "application/octet-stream"; + case ContentType::VIDEO_AVI: return "video/avi"; + case ContentType::VIDEO_MP4: return "video/mp4"; + case ContentType::VIDEO_WEBM: return "video/webm"; + case ContentType::AUDIO_BASIC: return "audio/basic"; + case ContentType::AUDIO_AIFF: return "audio/aiff"; + case ContentType::AUDIO_MPEG: return "audio/mpeg"; + case ContentType::AUDIO_MIDI: return "audio/midi"; + case ContentType::AUDIO_WAVE: return "audio/wave"; + case ContentType::AUDIO_FLAC: return "audio/wave"; + case ContentType::AUDIO_OPUS: return "audio/ogg"; + case ContentType::IMAGE_JPEG: return "image/jpeg"; + case ContentType::IMAGE_PNG: return "image/png"; + case ContentType::IMAGE_GIF: return "image/gif"; + case ContentType::IMAGE_BMP: return "image/bmp"; + case ContentType::IMAGE_WEBP: return "image/webp"; + } + return "application/octet-stream"; + } + + static int accumulate_string(char *data, int size, void *userdata) { + std::string *str = (std::string*)userdata; + str->append(data, size); + return 0; + } + + bool video_get_first_frame(const char *filepath, const char *destination_path) { + const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", filepath, "-vframes", "1", "-f", "singlejpeg", destination_path, nullptr }; + std::string ffmpeg_result; + if(exec_program(program_args, nullptr, nullptr) != 0) { + fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n"); + return false; + } + return true; + } + + // TODO: Remove dependency on ffprobe + static bool ffprobe_extract_metadata(const char *filepath, std::optional &dimensions, std::optional &duration_seconds) { + const char *program_args[] = { "ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "--", filepath, nullptr }; + std::string ffprobe_result; + if(exec_program(program_args, accumulate_string, &ffprobe_result) != 0) { + fprintf(stderr, "Failed to execute ffprobe, maybe its not installed?\n"); + return false; + } + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&ffprobe_result[0], &ffprobe_result[ffprobe_result.size()], &json_root, &json_errors)) { + fprintf(stderr, "ffprobe response parsing failed: %s\n", json_errors.c_str()); + return false; + } + + if(!json_root.isObject()) + return false; + + const Json::Value &streams_json = json_root["streams"]; + if(!streams_json.isArray()) + return false; + + for(const Json::Value &stream_json : streams_json) { + if(!stream_json.isObject()) + continue; + + const Json::Value &codec_type = stream_json["codec_type"]; + if(!codec_type.isString()) + continue; + + if(strcmp(codec_type.asCString(), "video") == 0) { + const Json::Value &width_json = stream_json["width"]; + const Json::Value &height_json = stream_json["height"]; + const Json::Value &duration_json = stream_json["duration"]; + if(width_json.isNumeric() && height_json.isNumeric()) + dimensions = { width_json.asInt(), height_json.asInt() }; + if(duration_json.isString()) + duration_seconds = atof(duration_json.asCString()); + break; + } else if(strcmp(codec_type.asCString(), "audio") == 0) { + const Json::Value &duration_json = stream_json["duration"]; + if(duration_json.isString()) + duration_seconds = atof(duration_json.asCString()); + // No break here because if there is video after this, we want it to overwrite this + } + } + + return true; + } + + FileAnalyzer::FileAnalyzer() : content_type(ContentType::UNKNOWN), file_size(0), loaded(false) { + + } + + bool FileAnalyzer::load_file(const char *filepath) { + if(loaded) { + fprintf(stderr, "File already loaded\n"); + return false; + } + + FILE *file = fopen(filepath, "rb"); + if(!file) { + perror(filepath); + return false; + } + + content_type = ContentType::UNKNOWN; + file_size = 0; + dimensions = std::nullopt; + duration_seconds = std::nullopt; + + struct stat stat; + if(fstat(fileno(file), &stat) == -1) { + perror(filepath); + fclose(file); + return false; + } + + file_size = stat.st_size; + + unsigned char magic_number_buffer[MAGIC_NUMBER_BUFFER_SIZE]; + size_t num_bytes_read = fread(magic_number_buffer, 1, sizeof(magic_number_buffer), file); + fclose(file); + + for(const MagicNumber &magic_number : magic_numbers) { + if(num_bytes_read >= magic_number.size) { + bool matching_magic_numbers = true; + for(size_t i = 0; i < magic_number.size; ++i) { + if(magic_number.data[i] != (int)magic_number_buffer[i] && (int)magic_number.data[i] != -1) { + matching_magic_numbers = false; + break; + } + } + if(matching_magic_numbers) { + content_type = magic_number.content_type; + break; + } + } + } + + if(content_type != ContentType::UNKNOWN) { + if(!ffprobe_extract_metadata(filepath, dimensions, duration_seconds)) { + // This is not an error, matrix allows files to be uploaded without metadata + fprintf(stderr, "Failed to extract metadata from file: %s, is ffprobe not installed?\n", filepath); + } + if(is_content_type_image(content_type)) + duration_seconds = std::nullopt; + } + + loaded = true; + return true; + } + + ContentType FileAnalyzer::get_content_type() const { + return content_type; + } + + size_t FileAnalyzer::get_file_size() const { + return file_size; + } + + std::optional FileAnalyzer::get_dimensions() const { + return dimensions; + } + + std::optional FileAnalyzer::get_duration_seconds() const { + return duration_seconds; + } +} \ No newline at end of file -- cgit v1.2.3