Files
oak-editor/app/codec/decoder.cpp
T

381 lines
9.9 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2020 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "decoder.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFileInfo>
#include "codec/ffmpeg/ffmpegdecoder.h"
#include "codec/oiio/oiiodecoder.h"
#include "codec/waveinput.h"
#include "codec/waveoutput.h"
#include "common/ffmpegutils.h"
#include "common/filefunctions.h"
#include "common/timecodefunctions.h"
#ifdef USE_OTIO
#include "task/project/loadotio/loadotio.h"
#endif
#include "task/taskmanager.h"
#include "project/project.h"
namespace olive {
QMutex Decoder::currently_conforming_mutex_;
QWaitCondition Decoder::currently_conforming_wait_cond_;
QVector<Decoder::CurrentlyConforming> Decoder::currently_conforming_;
Decoder::Decoder() :
stream_(nullptr)
{
}
bool Decoder::Open(Stream *fs)
{
QMutexLocker locker(&mutex_);
if (stream_) {
// Decoder is already open. Return TRUE if the stream is the stream we have, or FALSE if not.
if (stream_ == fs) {
return true;
} else {
qWarning() << "Tried to open a decoder that was already open with another stream";
return false;
}
} else {
// Stream was not open, try opening it now
if (fs == nullptr) {
// Cannot open null stream
qCritical() << "Decoder attempted to open null stream";
return false;
}
if (fs->footage()->decoder() != id()) {
qCritical() << "Tried to open footage in incorrect decoder";
return false;
}
// Set stream
stream_ = fs;
// Try open internal
if (OpenInternal()) {
return true;
} else {
// Unset stream
CloseInternal();
stream_ = nullptr;
return false;
}
}
}
FramePtr Decoder::RetrieveVideo(const rational &timecode, const int &divider)
{
QMutexLocker locker(&mutex_);
if (!stream_) {
qCritical() << "Can't retrieve video on a closed decoder";
return nullptr;
}
if (!SupportsVideo()) {
qCritical() << "Decoder doesn't support video";
return nullptr;
}
if (stream_->type() != Stream::kVideo) {
qCritical() << "Tried to retrieve video from a non-video stream";
return nullptr;
}
return RetrieveVideoInternal(timecode, divider);
}
SampleBufferPtr Decoder::RetrieveAudio(const TimeRange &range, const AudioParams &params, const QAtomicInt *cancelled)
{
QMutexLocker locker(&mutex_);
if (!stream_) {
qCritical() << "Can't retrieve audio on a closed decoder";
return nullptr;
}
if (!SupportsAudio()) {
qCritical() << "Decoder doesn't support audio";
return nullptr;
}
if (stream_->type() != Stream::kAudio) {
qCritical() << "Tried to retrieve audio from a non-audio stream";
return nullptr;
}
// Determine if we already have a conformed version
QString conform_filename = GetConformedFilename(params);
CurrentlyConforming want_conform = {stream_, params};
currently_conforming_mutex_.lock();
// Wait for conform to complete
while (currently_conforming_.contains(want_conform)) {
currently_conforming_wait_cond_.wait(&currently_conforming_mutex_);
}
// See if we got the conform
SampleBufferPtr buffer = RetrieveAudioFromConform(conform_filename, range);
if (!buffer) {
// We'll need to conform this ourselves
currently_conforming_.append(want_conform);
currently_conforming_mutex_.unlock();
// We conform to a different filename until it's done to make it clear even across sessions
// whether this conform is ready or not
QString working_fn = conform_filename;
working_fn.append(QStringLiteral(".working"));
if (ConformAudioInternal(working_fn, params, cancelled)) {
// Move file to standard conform name, making it clear this conform is ready for use
QFile::remove(conform_filename);
QFile::rename(working_fn, conform_filename);
// Return audio as planned
buffer = RetrieveAudioFromConform(conform_filename, range);
} else {
// Failed
qCritical() << "Failed to conform audio";
}
currently_conforming_mutex_.lock();
currently_conforming_.removeOne(want_conform);
currently_conforming_wait_cond_.wakeAll();
}
currently_conforming_mutex_.unlock();
return buffer;
}
void Decoder::Close()
{
QMutexLocker locker(&mutex_);
if (stream_) {
CloseInternal();
stream_ = nullptr;
} else {
qWarning() << "Tried to close a decoder that wasn't open";
}
}
/*
* DECODER STATIC PUBLIC MEMBERS
*/
QVector<DecoderPtr> ReceiveListOfAllDecoders()
{
QVector<DecoderPtr> decoders;
// The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last,
// since it supports so many formats and we presumably want to override those formats with a more specific decoder.
decoders.append(std::make_shared<OIIODecoder>());
decoders.append(std::make_shared<FFmpegDecoder>());
return decoders;
}
Footage* Decoder::Probe(Project* project, const QString &filename, const QAtomicInt* cancelled)
{
// Check for a valid filename
if (filename.isEmpty()) {
qWarning() << "Tried to probe media with an empty filename";
return nullptr;
}
// Check file exists
if (!QFileInfo::exists(filename)) {
qWarning() << "Tried to probe file that doesn't exist:" << filename;
return nullptr;
}
// Create list to iterate through
QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();
// Pass Footage through each Decoder's probe function
for (int i=0;i<decoder_list.size();i++) {
if (cancelled && *cancelled) {
return nullptr;
}
DecoderPtr decoder = decoder_list.at(i);
Footage* footage = decoder->Probe(filename, cancelled);
if (footage) {
QFileInfo file_info(filename);
footage->set_name(file_info.fileName());
footage->set_filename(filename);
footage->set_decoder(decoder->id());
footage->set_project(project);
footage->set_timestamp(file_info.lastModified().toMSecsSinceEpoch());
footage->SetValid();
// FIXME: Cache the results so we don't have to probe if this media is added a second time
return footage;
}
}
// We aren't able to use this Footage
return nullptr;
}
DecoderPtr Decoder::CreateFromID(const QString &id)
{
if (id.isEmpty()) {
return nullptr;
}
// Create list to iterate through
QVector<DecoderPtr> decoder_list = ReceiveListOfAllDecoders();
foreach (DecoderPtr d, decoder_list) {
if (d->id() == id) {
return d;
}
}
return nullptr;
}
QString Decoder::GetConformedFilename(const AudioParams &params)
{
QString index_fn = GetIndexFilename();
index_fn.append('.');
index_fn.append(QString::number(params.sample_rate()));
index_fn.append('.');
index_fn.append(QString::number(params.format()));
index_fn.append('.');
index_fn.append(QString::number(params.channel_layout()));
return index_fn;
}
QString Decoder::GetIndexFilename()
{
return QDir(stream_->footage()->project()->cache_path()).filePath(FileFunctions::GetUniqueFileIdentifier(stream()->footage()->filename()).append(QString::number(stream()->index())));
}
void Decoder::SignalProcessingProgress(const int64_t &ts)
{
if (stream()->duration() != AV_NOPTS_VALUE && stream()->duration() != 0) {
emit IndexProgress(static_cast<double>(ts) / static_cast<double>(stream()->duration()));
}
}
QString Decoder::TransformImageSequenceFileName(const QString &filename, const int64_t& number)
{
int digit_count = GetImageSequenceDigitCount(filename);
QFileInfo file_info(filename);
QString original_basename = file_info.baseName();
QString new_basename = original_basename.left(original_basename.size() - digit_count)
.append(QStringLiteral("%1").arg(number, digit_count, 10, QChar('0')));
return file_info.dir().filePath(file_info.fileName().replace(original_basename, new_basename));
}
int Decoder::GetImageSequenceDigitCount(const QString &filename)
{
QString basename = QFileInfo(filename).baseName();
// See if basename contains a number at the end
int digit_count = 0;
for (int i=basename.size()-1;i>=0;i--) {
if (basename.at(i).isDigit()) {
digit_count++;
} else {
break;
}
}
return digit_count;
}
int64_t Decoder::GetImageSequenceIndex(const QString &filename)
{
int digit_count = GetImageSequenceDigitCount(filename);
QFileInfo file_info(filename);
QString original_basename = file_info.baseName();
QString number_only = original_basename.mid(original_basename.size() - digit_count);
return number_only.toLongLong();
}
FramePtr Decoder::RetrieveVideoInternal(const rational &timecode, const int &divider)
{
Q_UNUSED(timecode)
Q_UNUSED(divider)
return nullptr;
}
bool Decoder::ConformAudioInternal(const QString& filename, const AudioParams &params, const QAtomicInt* cancelled)
{
Q_UNUSED(filename)
Q_UNUSED(cancelled)
Q_UNUSED(params)
return false;
}
SampleBufferPtr Decoder::RetrieveAudioFromConform(const QString &conform_filename, const TimeRange& range)
{
WaveInput input(conform_filename);
if (input.open()) {
const AudioParams& input_params = input.params();
// Read bytes from wav
QByteArray packed_data = input.read(input_params.time_to_bytes(range.in()),
input_params.time_to_bytes(range.length()));
input.close();
// Create sample buffer
SampleBufferPtr sample_buffer = SampleBuffer::CreateFromPackedData(input_params, packed_data);
return sample_buffer;
}
return nullptr;
}
}