aboutsummaryrefslogtreecommitdiff
path: root/src/FileAnalyzer.cpp
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-10-02 16:27:59 +0200
committerdec05eba <dec05eba@protonmail.com>2020-10-02 16:27:59 +0200
commitd9cb6885ab741ba69a966109cb05e26692143ce0 (patch)
treee4e356c182011bc389bc895665dab1507ecdb767 /src/FileAnalyzer.cpp
parent9e68dfad4449d5c0180e252fada6de56b4f405d1 (diff)
Matrix: add video/regular file upload
Diffstat (limited to 'src/FileAnalyzer.cpp')
-rw-r--r--src/FileAnalyzer.cpp227
1 files changed, 227 insertions, 0 deletions
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 <sys/stat.h>
+#include <stdio.h>
+#include <array>
+#include <json/reader.h> // TODO: Remove this dependency
+
+static const int MAGIC_NUMBER_BUFFER_SIZE = 36;
+
+namespace QuickMedia {
+ struct MagicNumber {
+ std::array<int, MAGIC_NUMBER_BUFFER_SIZE> 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<MagicNumber, 19> 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> &dimensions, std::optional<double> &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::CharReader> 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<Dimensions> FileAnalyzer::get_dimensions() const {
+ return dimensions;
+ }
+
+ std::optional<double> FileAnalyzer::get_duration_seconds() const {
+ return duration_seconds;
+ }
+} \ No newline at end of file