#include "../include/FileAnalyzer.hpp" #include "../include/AsyncImageLoader.hpp" #include "../include/Program.hpp" #include "../include/StringUtils.hpp" #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/ // 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{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, 12, ContentType::VIDEO_MP4 }, MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'p', '3', 'g', 'p', '4'}, 12, ContentType::VIDEO_MP4 }, MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'p', '3', 'g', 'p', '5'}, 12, ContentType::VIDEO_MP4 }, MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'm', 'p', '4', '2'}, 11, ContentType::VIDEO_MP4 }, MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', '3', 'g', 'p', '5'}, 11, ContentType::VIDEO_MP4 }, MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'p', 'q', 't'}, 10, ContentType::VIDEO_MP4 }, MagicNumber{ {0x00, 0x00, 0x01, 0xBA}, 4, ContentType::VIDEO_MPEG }, MagicNumber{ {0x00, 0x00, 0x01, 0xB3}, 4, ContentType::VIDEO_MPEG }, MagicNumber{ {0x1A, 0x45, 0xDF, 0xA3}, 4, ContentType::VIDEO_WEBM }, MagicNumber{ {'F', 'L', 'V', 0x01}, 4, ContentType::VIDEO_FLV }, MagicNumber{ {0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF}, 7, ContentType::VIDEO_WMV }, 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, -1, 'v', 'o', 'r', 'b', 'i', 's'}, 35, ContentType::AUDIO_VORBIS }, 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', 'P'}, 12, ContentType::IMAGE_WEBP }, MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'W', 'E', 'B', 'V', 'P'}, 13, ContentType::IMAGE_WEBP } }; bool is_content_type_video(ContentType content_type) { return content_type >= ContentType::VIDEO_AVI && content_type <= ContentType::VIDEO_WMV; } 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_MPEG: return "video/mpeg"; case ContentType::VIDEO_WEBM: return "video/webm"; case ContentType::VIDEO_FLV: return "video/x-flv"; case ContentType::VIDEO_WMV: return "video/x-ms-asf"; 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/flac"; case ContentType::AUDIO_VORBIS: return "audio/ogg"; case ContentType::AUDIO_OPUS: return "audio/opus"; 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"; } bool is_image_ext(const char *ext) { return strcase_equals(ext, ".jpg") || strcase_equals(ext, ".jpeg") || strcase_equals(ext, ".png") || strcase_equals(ext, ".gif") || strcase_equals(ext, ".webp"); } bool is_video_ext(const char *ext) { return strcase_equals(ext, ".webm") || strcase_equals(ext, ".mkv") || strcase_equals(ext, ".flv") || strcase_equals(ext, ".vob") || strcase_equals(ext, ".ogv") //|| strcase_equals(ext, ".ogg") || strcase_equals(ext, ".avi") //|| strcase_equals(ext, ".ts") || strcase_equals(ext, ".mov") || strcase_equals(ext, ".qt") || strcase_equals(ext, ".wmv") || strcase_equals(ext, ".mp4") || strcase_equals(ext, ".m4v") || strcase_equals(ext, ".mpg") || strcase_equals(ext, ".mpeg") || strcase_equals(ext, ".3gp"); } bool is_music_ext(const char *ext) { return strcase_equals(ext, ".aac") || strcase_equals(ext, ".alac") || strcase_equals(ext, ".flac") || strcase_equals(ext, ".m4a") || strcase_equals(ext, ".m4p") || strcase_equals(ext, ".mp3") || strcase_equals(ext, ".ogg") || strcase_equals(ext, ".oga") || strcase_equals(ext, ".mogg") || strcase_equals(ext, ".opus") || strcase_equals(ext, ".vox") || strcase_equals(ext, ".wav") || strcase_equals(ext, ".wma") || strcase_equals(ext, ".mid"); } bool video_get_middle_frame(const FileAnalyzer &file, const char *destination_path, int width, int height) { Path destination_path_tmp = destination_path; destination_path_tmp.append(".tmp.jpg"); // TODO: .png, but the below code also needs to be changed for that const int middle_seconds = file.get_duration_seconds().value_or(0.0) / 2.0; char middle_seconds_str[32]; snprintf(middle_seconds_str, sizeof(middle_seconds_str), "%d", middle_seconds); if(width > 0 && height > 0) { char size_arg_str[512]; snprintf(size_arg_str, sizeof(size_arg_str), "scale=%d:%d:force_original_aspect_ratio=decrease", width, height); const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-ss", middle_seconds_str, "-i", file.get_filepath().c_str(), "-vframes", "1", "-vf", size_arg_str, "--", destination_path_tmp.data.c_str(), nullptr }; if(exec_program(program_args, nullptr, nullptr) != 0) { fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n"); return false; } } else { const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-ss", middle_seconds_str, "-i", file.get_filepath().c_str(), "-vframes", "1", "--", destination_path_tmp.data.c_str(), nullptr }; if(exec_program(program_args, nullptr, nullptr) != 0) { fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n"); return false; } } return rename_atomic(destination_path_tmp.data.c_str(), destination_path) == 0; } // 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", "-show_format", "--", 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"]; if(width_json.isNumeric() && height_json.isNumeric()) dimensions = { width_json.asInt(), height_json.asInt() }; } } const Json::Value &format_json = json_root["format"]; if(!format_json.isObject()) return true; const Json::Value &duration_json = format_json["duration"]; if(duration_json.isString()) duration_seconds = atof(duration_json.asCString()); return true; } FileAnalyzer::FileAnalyzer() : content_type(ContentType::UNKNOWN), file_size(0), loaded(false), metadata_loaded(false) { } bool FileAnalyzer::load_file(const char *filepath, bool load_file_metadata) { 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; memset(&stat, 0, sizeof(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); if(feof(file) || num_bytes_read != sizeof(magic_number_buffer)) { perror(filepath); fclose(file); return false; } 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; } } } this->filepath = filepath; loaded = true; if(load_file_metadata && content_type != ContentType::UNKNOWN) load_metadata(); return true; } bool FileAnalyzer::load_metadata() { if(!loaded) { fprintf(stderr, "File not loaded\n"); return false; } if(metadata_loaded) return true; metadata_loaded = true; if(!ffprobe_extract_metadata(filepath.c_str(), 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.c_str()); return false; } if(is_content_type_image(content_type)) duration_seconds = std::nullopt; return true; } const std::string& FileAnalyzer::get_filepath() const { return filepath; } 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; } }