From 4e1faf96d5591c33c5d9b8ee7a8e6c54fd6c2a71 Mon Sep 17 00:00:00 2001 From: Louis O'Bryan Date: Fri, 4 Aug 2017 11:25:51 -0700 Subject: [PATCH] Add sample code to create MP4 with camm and video streams. --- example/muxing/Makefile | 32 ++++ example/muxing/camm_muxing.cc | 317 ++++++++++++++++++++++++++++++++++ example/muxing/sample.jpg | Bin 0 -> 31125 bytes 3 files changed, 349 insertions(+) create mode 100644 example/muxing/Makefile create mode 100644 example/muxing/camm_muxing.cc create mode 100644 example/muxing/sample.jpg diff --git a/example/muxing/Makefile b/example/muxing/Makefile new file mode 100644 index 0000000..d3d7528 --- /dev/null +++ b/example/muxing/Makefile @@ -0,0 +1,32 @@ +CC=g++ +CFLAGS += -Werror -Wextra -Wall -g +LDLIBS := $(shell pkg-config --libs mp4v2) $(LDLIBS) + +# Debug target to print variables. +print-%: + @echo '$*=$($*)' + +all: camm_muxing.o + $(CC) camm_muxing.o $(LDLIBS) $(CFLAGS) -o camm_muxing + +clean: + rm camm_muxing camm_muxing.o + +# You may need to add something similar to the following package config +# to your system's package config location. This may be located at, e.g. +# "/usr/local/lib/pkgconfig/mp4v2.pc" for mp4v2. (Try "man pkg-config".) +# +#prefix=/usr/local +#exec_prefix=${prefix} +#libdir=/usr/local/lib +#includedir=/usr/local/include +# +#Name: mp4v2 +#Description: MP4v2 library +#Version: 1.0.0 +#Requires: +#Requires.private: +#Conflicts: +#Libs: -L${libdir} -lmp4v2 +#Libs.private: +#Cflags: -I${includedir} diff --git a/example/muxing/camm_muxing.cc b/example/muxing/camm_muxing.cc new file mode 100644 index 0000000..ba75725 --- /dev/null +++ b/example/muxing/camm_muxing.cc @@ -0,0 +1,317 @@ +/* + * This code sample muxes an artificially generated video stream with a + * data stream for camera motion metadata (CAMM) into a single mp4 file. + * The sample can be built with the "make" command. + * After building the sample, it can be run as follows: + * + * > ./camm_muxing output_file.mp4 + * + * Note that this must be run in the sample directory as the "sample.jpg" file. + * + * The video stream is a single JPEG file for the entire video duration. It + * is for illustration purposes only. The calling code is expected to + * already have the encoded bytes of video frames to mux into the mp4 file. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FRAME_WIDTH 2880 +#define FRAME_HEIGHT 1800 +#define VIDEO_TIME_SECONDS 10 +#define VIDEO_TIME_SCALE 90000 +#define NUM_CAMM_SAMPLES 200 +#define BYTE_BUFFER_SIZE 1000 + +namespace { +// Number of bytes for the packet type in a CAMM data packet. +const int kCammPacketBytes = 4; +// These sizes are fixed when building with IEEE floating point format. +const int kMetadataTypeSizes[] = { + // 3 float angle_axis values. + 3 * sizeof(float), + // 2 int64 values for pixel_exposure_time and rolling_shutter_skew_time. + 2 * sizeof(uint64_t), + // 3 float gyro values. + 3 * sizeof(float), + // 3 float acceleration values. + 3 * sizeof(float), + // 3 float position values. + 3 * sizeof(float), + // 3 floats for latitude, longitude, and altitude. + 3 * sizeof(float), + // 3 doubles for time_gps_epoch, latitude, and longitude + // 1 int32 for gps_fix_type + // 7 floats for altitude, horizontal_accuracy, vertical_accuracy, + // vertical_east, vertical_north, vertical_up, and speed_accuracy. + 3 * sizeof(double) + sizeof(uint32_t) + 7 * sizeof(float), + // 3 floats for magnetic field. + 3 * sizeof(float)}; + +static uint64_t double_to_bytes(double d) { + uint64_t result; + memcpy(&result, &d, 8); + return htole64(result); +} + +static uint32_t float_to_bytes(float f) { + uint32_t result; + memcpy(&result, &f, 4); + return htole32(result); +} + +// Writes sample camm data in a byte stream format for a single packet. +// Returns the number of bytes written. +// In real code, you are not required to write all packet types like in this +// example. The pts of each packet also does not need to be equally spaced. +static int WriteSampleCammBytes(void *bytes, int packet_number) { + uint32_t angle_x, angle_y, angle_z, gyro_x, gyro_y, gyro_z, acceleration_x, + acceleration_y, acceleration_z, position_x, position_y, position_z, + latitude32, longitude32, gps_fix_type, horizontal_accuracy_meters, + vertical_accuracy_meters, vertical_east_velocity_mps, + vertical_north_velocity_mps, speed_accuracy_mps, altitude, + magnetic_field_x, magnetic_field_y, magnetic_field_z, + pixel_exposure_nanoseconds, rolling_shutter_skew_time, + vertical_up_velocity_mps; + uint64_t latitude64, longitude64, time_gps_epoch; + memset(bytes, 0, BYTE_BUFFER_SIZE); + uint16_t *camm_data = reinterpret_cast(bytes); + uint16_t packet_type = packet_number % 8; + // The packet type comes after the first 2 reserved bytes. + camm_data[1] = htole16(packet_type); + camm_data += 2; + switch (packet_type) { + case 0: + angle_x = float_to_bytes(M_PI / 2); + angle_y = float_to_bytes(-M_PI / 2); + angle_z = + float_to_bytes(fmod(packet_number * M_PI / 20, 2 * M_PI) - M_PI); + memcpy(camm_data, &angle_x, 4); + memcpy(camm_data + 2, &angle_y, 4); + memcpy(camm_data + 4, &angle_z, 4); + break; + case 1: + pixel_exposure_nanoseconds = double_to_bytes(500); + rolling_shutter_skew_time = double_to_bytes(300); + memcpy(camm_data, &pixel_exposure_nanoseconds, 8); + memcpy(camm_data + 4, &rolling_shutter_skew_time, 8); + break; + case 2: + gyro_x = float_to_bytes(M_PI / 20); + gyro_y = float_to_bytes(2 * M_PI / 20); + gyro_z = float_to_bytes(3 * M_PI / 20); + memcpy(camm_data, &gyro_x, 4); + memcpy(camm_data + 2, &gyro_y, 4); + memcpy(camm_data + 4, &gyro_z, 4); + break; + case 3: + acceleration_x = float_to_bytes(0.1); + acceleration_y = float_to_bytes(0.2); + acceleration_z = float_to_bytes(0.3); + memcpy(camm_data, &acceleration_x, 4); + memcpy(camm_data + 2, &acceleration_y, 4); + memcpy(camm_data + 4, &acceleration_z, 4); + break; + case 4: + position_x = 0; + position_y = 0; + position_z = 0; + memcpy(camm_data, &position_x, 4); + memcpy(camm_data + 2, &position_y, 4); + memcpy(camm_data + 4, &position_z, 4); + break; + case 5: + latitude32 = float_to_bytes(37.454356 + .001 * packet_number); + longitude32 = float_to_bytes(-122.167477 + 0.001 * packet_number); + altitude = 0; + memcpy(camm_data, &latitude32, 4); + memcpy(camm_data + 2, &longitude32, 4); + memcpy(camm_data + 4, &altitude, 4); + break; + case 6: + time_gps_epoch = double_to_bytes(1500507374.825 + packet_number); + memcpy(camm_data, &time_gps_epoch, 8); + camm_data += 4; + gps_fix_type = 0; + memcpy(camm_data, &gps_fix_type, 4); + camm_data += 2; + latitude64 = double_to_bytes(37.454356 + .001 * packet_number); + memcpy(camm_data, &latitude64, 8); + camm_data += 4; + longitude64 = double_to_bytes(-122.167477 + .001 * packet_number); + memcpy(camm_data, &longitude64, 8); + camm_data += 4; + altitude = 0; + memcpy(camm_data, &altitude, 4); + camm_data += 2; + horizontal_accuracy_meters = float_to_bytes(7.5); + memcpy(camm_data, &horizontal_accuracy_meters, 4); + camm_data += 2; + vertical_accuracy_meters = float_to_bytes(10.5); + memcpy(camm_data, &vertical_accuracy_meters, 4); + camm_data += 2; + vertical_east_velocity_mps = float_to_bytes(1.1); + memcpy(camm_data, &vertical_east_velocity_mps, 4); + camm_data += 2; + vertical_north_velocity_mps = float_to_bytes(1.1); + memcpy(camm_data, &vertical_north_velocity_mps, 4); + camm_data += 2; + vertical_up_velocity_mps = 0; + memcpy(camm_data, &vertical_up_velocity_mps, 4); + camm_data += 2; + speed_accuracy_mps = float_to_bytes(2.5); + memcpy(camm_data, &speed_accuracy_mps, 4); + break; + case 7: + magnetic_field_x = float_to_bytes(0.01); + magnetic_field_y = float_to_bytes(0.01); + magnetic_field_z = float_to_bytes(0.01); + memcpy(camm_data, &magnetic_field_x, 4); + memcpy(camm_data + 2, &magnetic_field_y, 4); + memcpy(camm_data + 4, &magnetic_field_z, 4); + break; + default: + break; + } + return kCammPacketBytes + kMetadataTypeSizes[packet_type]; +} + +static void WriteCammPacket(MP4FileHandle handle, int camm_track_id, int pts, + int packet_num, char *buffer) { + int packet_size = WriteSampleCammBytes(buffer, packet_num); + if (!MP4WriteSample(handle, camm_track_id, + reinterpret_cast(buffer), packet_size, + /* duration */ 0, + /* rendering_offset */ pts, + /* is_sync_sample */ true)) { + printf("Failed to write camm track sample\n"); + exit(1); + } +} + +static void WriteVideoFrame(MP4FileHandle handle, int video_track_id, + const std::vector &jpg_bytes, int pts, + int duration) { + // This code sample doesn't illustrate how to encode video. An encoder + // for h264 or another format should be used to do that, and then call + // MP4WriteSample with the bytes returned from the encoder. + if (!MP4WriteSample(handle, video_track_id, + reinterpret_cast(jpg_bytes.data()), + /* num_bytes */ jpg_bytes.size(), + /* duration */ VIDEO_TIME_SCALE, + /* rendering_offset */ pts, + /* is_sync_sample */ true)) { + printf("Failed to write mp4 sample to video track\n"); + exit(1); + } +} + +static void WriteStreams(MP4FileHandle handle, int video_track_id, + const std::vector &jpg_bytes, + int camm_track_id) { + char camm_buffer[BYTE_BUFFER_SIZE]; + int video_pts = 0; + int camm_pts = 0; + int camm_packet_num = 0; + int max_pts = VIDEO_TIME_SECONDS * VIDEO_TIME_SCALE; + while (video_pts < max_pts && camm_pts < max_pts) { + if (video_pts <= camm_pts) { + WriteVideoFrame(handle, video_track_id, jpg_bytes, video_pts, + VIDEO_TIME_SCALE); + video_pts += VIDEO_TIME_SCALE; + } else { + WriteCammPacket(handle, camm_track_id, camm_pts, camm_packet_num, + camm_buffer); + camm_pts += max_pts / NUM_CAMM_SAMPLES; + camm_packet_num++; + } + } +} + +static int AddVideoTrack(MP4FileHandle handle) { + printf("Adding video track\n"); + int video_track_id = MP4AddVideoTrack( + handle, VIDEO_TIME_SCALE, VIDEO_TIME_SCALE * VIDEO_TIME_SECONDS, + FRAME_WIDTH, FRAME_HEIGHT, MP4_JPEG_VIDEO_TYPE); + if (video_track_id == MP4_INVALID_TRACK_ID) { + printf("Can't add video track\n"); + exit(1); + } + return video_track_id; +} + +static int AddCammTrack(MP4FileHandle handle) { + printf("Adding camm track\n"); + int data_track_id = MP4AddTrack(handle, "camm", VIDEO_TIME_SCALE); + if (data_track_id == MP4_INVALID_TRACK_ID) { + printf("Can't add camm data track\n"); + exit(1); + } + if (!MP4SetTrackName(handle, data_track_id, "camm")) { + printf("Couldn't set the track name of the CAMM track.\n"); + exit(1); + } + return data_track_id; +} + +// Reads the contents of the sample jpeg file into memory. +static std::vector ReadSampleFileBytes() { + struct stat buffer; + const char *filename = "sample.jpg"; + if (stat(filename, &buffer) != 0) { + printf( + "Sample file 'sample.jpg' doesn't exist\n" + "Make sure it is in the directory and then run the program\n" + "again.\n"); + exit(1); + } + std::ifstream ifs(filename, std::ios::binary | std::ios::ate); + std::ifstream::pos_type pos = ifs.tellg(); + std::vector result(pos); + ifs.seekg(0, std::ios::beg); + ifs.read(&result[0], pos); + return result; +} + +static void VerifyProgramArgs(int argc, char **argv) { + if (argc != 2) { + printf( + "Usage: %s file.mp4\n" + "file.mp4 is the output location of the generated video\n", + argv[0]); + exit(1); + } +} +} // namespace + +int main(int argc, char **argv) { +#ifndef __STDC_IEC_559__ + fprintf(stderr, + "Please ensure your compiler uses IEEE 754 \n" + "floating point representation. If so, you may safely comment out \n" + "this guard.\n"); + exit(1); +#endif + + VerifyProgramArgs(argc, argv); + std::vector jpg_bytes = ReadSampleFileBytes(); + MP4FileHandle handle = MP4Create(argv[1], /* flags */ 0); + int video_track_id = AddVideoTrack(handle); + int camm_track_id = AddCammTrack(handle); + + WriteStreams(handle, video_track_id, jpg_bytes, camm_track_id); + + printf("Dumping MP4 file information:\n"); + MP4Dump(handle); + MP4Close(handle); + printf("Done!\n"); + return 0; +} diff --git a/example/muxing/sample.jpg b/example/muxing/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c76d5528995d7b73b4918e981dff219609e75b78 GIT binary patch literal 31125 zcmeI(KTE?v9LDkAC1_I>Oh8vTofK432M1B2l(sYuN)b8>N~wq_+JcjB$X6l007w5U z0Vm%?&P%ltDx^c#?`27j=k9W!{V}g55kIas>!Os1vX7WlcB`%0!O2l;M|aLPF0Riz zx?kB0^=j|7d)3j+xS`|YT0{4@x7OD6P4D!wdv~w*bR`UD%!gF%UXzV%lgW0ltId-H zUdfx?ch!nGfpP<7o@H@xQOS=|HfZ^>9(doSBFW*~q70tg_000IagfB*srAb