Adding clip serialization implementation, tests

This commit is contained in:
Brad Davis 2015-11-06 14:40:10 -08:00
parent 8eea7ff67c
commit 4513b638db
17 changed files with 427 additions and 58 deletions

View file

@ -16,33 +16,34 @@
using namespace recording;
Clip::Pointer Clip::fromFile(const QString& filePath) {
return std::make_shared<FileClip>(filePath);
auto result = std::make_shared<FileClip>(filePath);
if (result->frameCount() == 0) {
return Clip::Pointer();
}
return result;
}
void Clip::toFile(Clip::Pointer clip, const QString& filePath) {
// FIXME
void Clip::toFile(const QString& filePath, Clip::Pointer clip) {
FileClip::write(filePath, clip->duplicate());
}
Clip::Pointer Clip::newClip() {
return std::make_shared<BufferClip>();
}
Clip::Pointer Clip::duplicate() {
Clip::Pointer result = std::make_shared<BufferClip>();
Locker lock(_mutex);
float currentPosition = position();
seek(0);
Frame::Pointer frame = nextFrame();
while (frame) {
result->appendFrame(frame);
result->addFrame(frame);
frame = nextFrame();
}
seek(currentPosition);
return result;
}
#if 0
Clip::Pointer Clip::fromIODevice(QIODevice * device) {
return std::make_shared<IOClip>(device);
}
void Clip::fromIODevice(Clip::Pointer clip, QIODevice * device) {
}
#endif

View file

@ -12,35 +12,44 @@
#include "Forward.h"
#include <mutex>
#include <QtCore/QObject>
class QIODevice;
namespace recording {
class Clip : public QObject {
class Clip {
public:
using Pointer = std::shared_ptr<Clip>;
Clip(QObject* parent = nullptr) : QObject(parent) {}
virtual ~Clip() {}
Pointer duplicate();
virtual float duration() const = 0;
virtual size_t frameCount() const = 0;
virtual void seek(float offset) = 0;
virtual float position() const = 0;
virtual FramePointer peekFrame() const = 0;
virtual FramePointer nextFrame() = 0;
virtual void skipFrame() = 0;
virtual void appendFrame(FramePointer) = 0;
virtual void addFrame(FramePointer) = 0;
static Pointer fromFile(const QString& filePath);
static void toFile(Pointer clip, const QString& filePath);
static void toFile(const QString& filePath, Pointer clip);
static Pointer newClip();
protected:
using Mutex = std::recursive_mutex;
using Locker = std::unique_lock<Mutex>;
virtual void reset() = 0;
mutable Mutex _mutex;
};
}

View file

@ -29,6 +29,10 @@ public:
float timeOffset { 0 };
QByteArray data;
Frame() {}
Frame(FrameType type, float timeOffset, const QByteArray& data)
: type(type), timeOffset(timeOffset), data(data) {}
static FrameType registerFrameType(const QString& frameTypeName);
static QMap<QString, FrameType> getFrameTypes();
static QMap<FrameType, QString> getFrameTypeNames();

View file

@ -0,0 +1,11 @@
//
// Created by Bradley Austin Davis 2015/10/11
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Logging.h"
Q_LOGGING_CATEGORY(recordingLog, "hifi.recording")

View file

@ -0,0 +1,16 @@
//
// Created by Bradley Austin Davis 2015/10/11
// Copyright 2015 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_Controllers_Logging_h
#define hifi_Controllers_Logging_h
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(recordingLog)
#endif

View file

@ -51,7 +51,7 @@ void Recorder::recordFrame(FrameType type, QByteArray frameData) {
frame->type = type;
frame->data = frameData;
frame->timeOffset = (float)(_elapsed + _timer.elapsed()) / MSECS_PER_SECOND;
_clip->appendFrame(frame);
_clip->addFrame(frame);
}
ClipPointer Recorder::getClip() {

View file

@ -51,11 +51,15 @@ FramePointer BufferClip::nextFrame() {
return result;
}
void BufferClip::appendFrame(FramePointer newFrame) {
void BufferClip::addFrame(FramePointer newFrame) {
if (newFrame->timeOffset < 0.0f) {
throw std::runtime_error("Frames may not have negative time offsets");
}
auto currentPosition = position();
seek(newFrame->timeOffset);
{
Locker lock(_mutex);
_frames.insert(_frames.begin() + _frameIndex, newFrame);
}
seek(currentPosition);
@ -72,3 +76,15 @@ void BufferClip::reset() {
Locker lock(_mutex);
_frameIndex = 0;
}
float BufferClip::duration() const {
if (_frames.empty()) {
return 0;
}
return (*_frames.rbegin())->timeOffset;
}
size_t BufferClip::frameCount() const {
return _frames.size();
}

View file

@ -20,25 +20,23 @@ class BufferClip : public Clip {
public:
using Pointer = std::shared_ptr<BufferClip>;
BufferClip(QObject* parent = nullptr) : Clip(parent) {}
virtual ~BufferClip() {}
virtual float duration() const override;
virtual size_t frameCount() const override;
virtual void seek(float offset) override;
virtual float position() const override;
virtual FramePointer peekFrame() const override;
virtual FramePointer nextFrame() override;
virtual void skipFrame() override;
virtual void appendFrame(FramePointer) override;
virtual void addFrame(FramePointer) override;
private:
using Mutex = std::mutex;
using Locker = std::unique_lock<Mutex>;
virtual void reset() override;
std::vector<FramePointer> _frames;
mutable Mutex _mutex;
mutable size_t _frameIndex { 0 };
};

View file

@ -8,42 +8,197 @@
#include "FileClip.h"
#include "../Frame.h"
#include <algorithm>
#include <QtCore/QDebug>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <Finally.h>
#include "../Frame.h"
#include "../Logging.h"
using namespace recording;
static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(float) + sizeof(uint16_t) + 1;
static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(float) + sizeof(uint16_t);
FileClip::FileClip(const QString& fileName, QObject* parent) : Clip(parent), _file(fileName) {
auto size = _file.size();
_map = _file.map(0, size, QFile::MapPrivateOption);
static const QString FRAME_TYPE_MAP = QStringLiteral("frameTypes");
auto current = _map;
using FrameHeaderList = std::list<FileClip::FrameHeader>;
using FrameTranslationMap = QMap<FrameType, FrameType>;
FrameTranslationMap parseTranslationMap(const QJsonDocument& doc) {
FrameTranslationMap results;
auto headerObj = doc.object();
if (headerObj.contains(FRAME_TYPE_MAP)) {
auto frameTypeObj = headerObj[FRAME_TYPE_MAP].toObject();
auto currentFrameTypes = Frame::getFrameTypes();
for (auto frameTypeName : frameTypeObj.keys()) {
qDebug() << frameTypeName;
if (!currentFrameTypes.contains(frameTypeName)) {
continue;
}
FrameType currentTypeEnum = currentFrameTypes[frameTypeName];
FrameType storedTypeEnum = static_cast<FrameType>(frameTypeObj[frameTypeName].toInt());
results[storedTypeEnum] = currentTypeEnum;
}
}
return results;
}
FrameHeaderList parseFrameHeaders(uchar* const start, const qint64& size) {
using FrameHeader = FileClip::FrameHeader;
FrameHeaderList results;
auto current = start;
auto end = current + size;
// Read all the frame headers
while (end - current < MINIMUM_FRAME_SIZE) {
// FIXME move to Frame::readHeader?
while (end - current >= MINIMUM_FRAME_SIZE) {
FrameHeader header;
memcpy(&(header.type), current, sizeof(FrameType));
current += sizeof(FrameType);
memcpy(&(header.timeOffset), current, sizeof(FrameType));
memcpy(&(header.timeOffset), current, sizeof(float));
current += sizeof(float);
memcpy(&(header.size), current, sizeof(uint16_t));
current += sizeof(uint16_t);
header.fileOffset = current - _map;
header.fileOffset = current - start;
if (end - current < header.size) {
current = end;
break;
}
_frameHeaders.push_back(header);
current += header.size;
results.push_back(header);
}
return results;
}
FileClip::FileClip(const QString& fileName) : _file(fileName) {
auto size = _file.size();
bool opened = _file.open(QIODevice::ReadOnly);
if (!opened) {
qCWarning(recordingLog) << "Unable to open file " << fileName;
return;
}
_map = _file.map(0, size, QFile::MapPrivateOption);
if (!_map) {
qCWarning(recordingLog) << "Unable to map file " << fileName;
return;
}
FrameHeaderList parsedFrameHeaders = parseFrameHeaders(_map, size);
// Verify that at least one frame exists and that the first frame is a header
if (0 == parsedFrameHeaders.size()) {
qWarning() << "No frames found, invalid file";
return;
}
// Grab the file header
{
auto fileHeaderFrameHeader = *parsedFrameHeaders.begin();
parsedFrameHeaders.pop_front();
if (fileHeaderFrameHeader.type != Frame::TYPE_HEADER) {
qWarning() << "Missing header frame, invalid file";
return;
}
QByteArray fileHeaderData((char*)_map + fileHeaderFrameHeader.fileOffset, fileHeaderFrameHeader.size);
_fileHeader = QJsonDocument::fromBinaryData(fileHeaderData);
}
// Find the type enum translation map and fix up the frame headers
{
FrameTranslationMap translationMap = parseTranslationMap(_fileHeader);
if (translationMap.empty()) {
qWarning() << "Header missing frame type map, invalid file";
return;
}
// Update the loaded headers with the frame data
_frameHeaders.reserve(parsedFrameHeaders.size());
for (auto& frameHeader : parsedFrameHeaders) {
if (!translationMap.contains(frameHeader.type)) {
continue;
}
frameHeader.type = translationMap[frameHeader.type];
_frameHeaders.push_back(frameHeader);
}
}
}
// FIXME move to frame?
bool writeFrame(QIODevice& output, const Frame& frame) {
auto written = output.write((char*)&(frame.type), sizeof(FrameType));
if (written != sizeof(FrameType)) {
return false;
}
written = output.write((char*)&(frame.timeOffset), sizeof(float));
if (written != sizeof(float)) {
return false;
}
uint16_t dataSize = frame.data.size();
written = output.write((char*)&dataSize, sizeof(uint16_t));
if (written != sizeof(uint16_t)) {
return false;
}
if (dataSize != 0) {
written = output.write(frame.data);
if (written != dataSize) {
return false;
}
}
return true;
}
bool FileClip::write(const QString& fileName, Clip::Pointer clip) {
qCDebug(recordingLog) << "Writing clip to file " << fileName;
if (0 == clip->frameCount()) {
return false;
}
QFile outputFile(fileName);
if (!outputFile.open(QFile::Truncate | QFile::WriteOnly)) {
return false;
}
Finally closer([&] { outputFile.close(); });
{
auto frameTypes = Frame::getFrameTypes();
QJsonObject frameTypeObj;
for (const auto& frameTypeName : frameTypes.keys()) {
frameTypeObj[frameTypeName] = frameTypes[frameTypeName];
}
QJsonObject rootObject;
rootObject.insert(FRAME_TYPE_MAP, frameTypeObj);
QByteArray headerFrameData = QJsonDocument(rootObject).toBinaryData();
if (!writeFrame(outputFile, Frame({ Frame::TYPE_HEADER, 0, headerFrameData }))) {
return false;
}
}
clip->seek(0);
for (auto frame = clip->nextFrame(); frame; frame = clip->nextFrame()) {
if (!writeFrame(outputFile, *frame)) {
return false;
}
}
outputFile.close();
return true;
}
FileClip::~FileClip() {
Locker lock(_mutex);
_file.unmap(_map);
_map = nullptr;
if (_file.isOpen()) {
_file.close();
}
}
void FileClip::seek(float offset) {
@ -72,7 +227,9 @@ FramePointer FileClip::readFrame(uint32_t frameIndex) const {
const FrameHeader& header = _frameHeaders[frameIndex];
result->type = header.type;
result->timeOffset = header.timeOffset;
result->data.insert(0, reinterpret_cast<char*>(_map)+header.fileOffset, header.size);
if (header.size) {
result->data.insert(0, reinterpret_cast<char*>(_map)+header.fileOffset, header.size);
}
}
return result;
}
@ -99,7 +256,18 @@ void FileClip::reset() {
_frameIndex = 0;
}
void FileClip::appendFrame(FramePointer) {
void FileClip::addFrame(FramePointer) {
throw std::runtime_error("File clips are read only");
}
float FileClip::duration() const {
if (_frameHeaders.empty()) {
return 0;
}
return _frameHeaders.rbegin()->timeOffset;
}
size_t FileClip::frameCount() const {
return _frameHeaders.size();
}

View file

@ -13,6 +13,7 @@
#include "../Clip.h"
#include <QtCore/QFile>
#include <QtCore/QJsonDocument>
#include <mutex>
@ -22,22 +23,25 @@ class FileClip : public Clip {
public:
using Pointer = std::shared_ptr<FileClip>;
FileClip(const QString& file, QObject* parent = nullptr);
FileClip(const QString& file);
virtual ~FileClip();
virtual float duration() const override;
virtual size_t frameCount() const override;
virtual void seek(float offset) override;
virtual float position() const override;
virtual FramePointer peekFrame() const override;
virtual FramePointer nextFrame() override;
virtual void appendFrame(FramePointer) override;
virtual void skipFrame() override;
virtual void addFrame(FramePointer) override;
private:
using Mutex = std::mutex;
using Locker = std::unique_lock<Mutex>;
const QJsonDocument& getHeader() {
return _fileHeader;
}
virtual void reset() override;
static bool write(const QString& filePath, Clip::Pointer clip);
struct FrameHeader {
FrameType type;
@ -46,15 +50,20 @@ private:
quint64 fileOffset;
};
using FrameHeaders = std::vector<FrameHeader>;
private:
virtual void reset() override;
using FrameHeaderVector = std::vector<FrameHeader>;
FramePointer readFrame(uint32_t frameIndex) const;
mutable Mutex _mutex;
QJsonDocument _fileHeader;
QFile _file;
uint32_t _frameIndex { 0 };
uchar* _map;
FrameHeaders _frameHeaders;
uchar* _map { nullptr };
FrameHeaderVector _frameHeaders;
};
}

View file

@ -1,10 +1,16 @@
set(TARGET_NAME recording-test)
# This is not a testcase -- just set it up as a regular hifi project
setup_hifi_project(Test)
set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/")
link_hifi_libraries(shared recording)
copy_dlls_beside_windows_executable()
# FIXME convert to unit tests
# Declare dependencies
macro (setup_testcase_dependencies)
# link in the shared libraries
link_hifi_libraries(shared recording)
copy_dlls_beside_windows_executable()
endmacro ()
setup_hifi_testcase()
#macro (setup_testcase_dependencies)
# # link in the shared libraries
# link_hifi_libraries(shared recording)
#
# copy_dlls_beside_windows_executable()
#endmacro ()
#setup_hifi_testcase()

View file

@ -11,6 +11,8 @@
#ifndef hifi_Constants_h
#define hifi_Constants_h
#include <QtCore/QString>
static const QString HEADER_NAME = "com.highfidelity.recording.Header";
static const QString TEST_NAME = "com.highfidelity.recording.Test";

View file

@ -8,6 +8,9 @@
#include "FrameTests.h"
#include "Constants.h"
#if 0
#include "../QTestExtensions.h"
#include <recording/Frame.h>
@ -27,3 +30,4 @@ void FrameTests::registerFrameTypeTest() {
QCOMPARE(backMap[recording::Frame::TYPE_HEADER], HEADER_NAME);
}
#endif

View file

@ -10,6 +10,7 @@
#ifndef hifi_FrameTests_h
#define hifi_FrameTests_h
#if 0
#include <QtTest/QtTest>
class FrameTests : public QObject {
@ -18,4 +19,6 @@ private slots:
void registerFrameTypeTest();
};
#endif
#endif // hifi_FrameTests_h

View file

@ -8,6 +8,9 @@
#include "RecorderTests.h"
#include "Constants.h"
#if 0
#include "../QTestExtensions.h"
#include <recording/Recorder.h>
@ -23,3 +26,4 @@ void RecorderTests::recorderTest() {
//QCOMPARE(recoreder.isRecording(), false);
}
#endif

View file

@ -10,6 +10,8 @@
#ifndef hifi_RecorderTests_h
#define hifi_RecorderTests_h
#if 0
#include <QtTest/QtTest>
class RecorderTests : public QObject {
@ -19,3 +21,5 @@ private slots:
};
#endif
#endif

View file

@ -0,0 +1,114 @@
#include <QtGlobal>
#include <QtTest/QtTest>
#include <QtCore/QTemporaryFile>
#include <QtCore/QString>
#ifdef Q_OS_WIN32
#include <Windows.h>
#endif
#include <recording/Clip.h>
#include <recording/Frame.h>
#include "Constants.h"
#define QVERIFY Q_ASSERT
using namespace recording;
FrameType TEST_FRAME_TYPE { Frame::TYPE_INVALID };
void testFrameTypeRegistration() {
TEST_FRAME_TYPE = Frame::registerFrameType(TEST_NAME);
QVERIFY(TEST_FRAME_TYPE != Frame::TYPE_INVALID);
QVERIFY(TEST_FRAME_TYPE != Frame::TYPE_HEADER);
auto forwardMap = recording::Frame::getFrameTypes();
QVERIFY(forwardMap.count(TEST_NAME) == 1);
QVERIFY(forwardMap[TEST_NAME] == TEST_FRAME_TYPE);
QVERIFY(forwardMap[HEADER_NAME] == recording::Frame::TYPE_HEADER);
auto backMap = recording::Frame::getFrameTypeNames();
QVERIFY(backMap.count(TEST_FRAME_TYPE) == 1);
QVERIFY(backMap[TEST_FRAME_TYPE] == TEST_NAME);
QVERIFY(backMap[recording::Frame::TYPE_HEADER] == HEADER_NAME);
}
void testFilePersist() {
QTemporaryFile file;
QString fileName;
if (file.open()) {
fileName = file.fileName();
file.close();
}
auto readClip = Clip::fromFile(fileName);
QVERIFY(Clip::Pointer() == readClip);
auto writeClip = Clip::newClip();
writeClip->addFrame(std::make_shared<Frame>(TEST_FRAME_TYPE, 5.0f, QByteArray()));
QVERIFY(writeClip->frameCount() == 1);
QVERIFY(writeClip->duration() == 5.0f);
Clip::toFile(fileName, writeClip);
readClip = Clip::fromFile(fileName);
QVERIFY(readClip != Clip::Pointer());
QVERIFY(readClip->frameCount() == 1);
QVERIFY(readClip->duration() == 5.0f);
readClip->seek(0);
writeClip->seek(0);
size_t count = 0;
for (auto readFrame = readClip->nextFrame(), writeFrame = writeClip->nextFrame(); readFrame && writeFrame;
readFrame = readClip->nextFrame(), writeFrame = writeClip->nextFrame(), ++count) {
QVERIFY(readFrame->type == writeFrame->type);
QVERIFY(readFrame->timeOffset == writeFrame->timeOffset);
QVERIFY(readFrame->data == writeFrame->data);
}
QVERIFY(readClip->frameCount() == count);
writeClip = Clip::newClip();
writeClip->addFrame(std::make_shared<Frame>(TEST_FRAME_TYPE, 5.0f, QByteArray()));
// Simulate an unknown frametype
writeClip->addFrame(std::make_shared<Frame>(Frame::TYPE_INVALID - 1, 10.0f, QByteArray()));
QVERIFY(writeClip->frameCount() == 2);
QVERIFY(writeClip->duration() == 10.0f);
Clip::toFile(fileName, writeClip);
// Verify that the read version of the clip ignores the unknown frame type
readClip = Clip::fromFile(fileName);
QVERIFY(readClip != Clip::Pointer());
QVERIFY(readClip->frameCount() == 1);
QVERIFY(readClip->duration() == 5.0f);
}
void testClipOrdering() {
auto writeClip = Clip::newClip();
// simulate our of order addition of frames
writeClip->addFrame(std::make_shared<Frame>(TEST_FRAME_TYPE, 10.0f, QByteArray()));
writeClip->addFrame(std::make_shared<Frame>(TEST_FRAME_TYPE, 5.0f, QByteArray()));
QVERIFY(writeClip->frameCount() == 2);
QVERIFY(writeClip->duration() == 10.0f);
QVERIFY(std::numeric_limits<float>::max() == writeClip->position());
writeClip->seek(0);
QVERIFY(5.0f == writeClip->position());
float lastFrameTimeOffset { 0 };
for (auto writeFrame = writeClip->nextFrame(); writeFrame; writeFrame = writeClip->nextFrame()) {
QVERIFY(writeClip->position() >= lastFrameTimeOffset);
}
}
#ifdef Q_OS_WIN32
void myMessageHandler(QtMsgType type, const QMessageLogContext & context, const QString & msg) {
OutputDebugStringA(msg.toLocal8Bit().toStdString().c_str());
OutputDebugStringA("\n");
}
#endif
int main(int, const char**) {
#ifdef Q_OS_WIN32
qInstallMessageHandler(myMessageHandler);
#endif
testFrameTypeRegistration();
testFilePersist();
testClipOrdering();
}