mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-10 11:40:09 -04:00
430 lines
14 KiB
C++
430 lines
14 KiB
C++
// This file is part of Moonfire NVR, a security camera digital video recorder.
|
|
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
|
|
//
|
|
// 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.
|
|
//
|
|
// In addition, as a special exception, the copyright holders give
|
|
// permission to link the code of portions of this program with the
|
|
// OpenSSL library under certain conditions as described in each
|
|
// individual source file, and distribute linked combinations including
|
|
// the two.
|
|
//
|
|
// You must obey the GNU General Public License in all respects for all
|
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
|
// exception, you may extend this exception to your version of the
|
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
|
// so, delete this exception statement from your version. If you delete
|
|
// this exception statement from all source files in the program, then
|
|
// also delete it here.
|
|
//
|
|
// 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/>.
|
|
//
|
|
// ffmpeg.cc: See ffmpeg.h for description.
|
|
|
|
#include "ffmpeg.h"
|
|
|
|
#include <mutex>
|
|
|
|
extern "C" {
|
|
#include <libavutil/buffer.h>
|
|
#include <libavutil/mathematics.h>
|
|
#include <libavutil/version.h>
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavcodec/version.h>
|
|
#include <libavformat/version.h>
|
|
} // extern "C"
|
|
|
|
#include <gflags/gflags.h>
|
|
|
|
#include "string.h"
|
|
|
|
// libav lacks this ffmpeg constant.
|
|
#ifndef AV_ERROR_MAX_STRING_SIZE
|
|
#define AV_ERROR_MAX_STRING_SIZE 64
|
|
#endif
|
|
|
|
DEFINE_int32(avlevel, AV_LOG_INFO,
|
|
"maximum logging level for ffmpeg/libav; "
|
|
"higher levels will be ignored.");
|
|
|
|
namespace moonfire_nvr {
|
|
|
|
namespace {
|
|
|
|
std::string AvError2Str(re2::StringPiece function, int err) {
|
|
char str[AV_ERROR_MAX_STRING_SIZE];
|
|
if (av_strerror(err, str, sizeof(str)) == 0) {
|
|
return StrCat(function, ": ", str);
|
|
}
|
|
return StrCat(function, ": unknown error ", err);
|
|
}
|
|
|
|
struct Dictionary {
|
|
Dictionary() {}
|
|
Dictionary(const Dictionary &) = delete;
|
|
Dictionary &operator=(const Dictionary &) = delete;
|
|
~Dictionary() { av_dict_free(&dict); }
|
|
|
|
bool Set(const char *key, const char *value, std::string *error_message) {
|
|
int ret = av_dict_set(&dict, key, value, 0);
|
|
if (ret < 0) {
|
|
*error_message = AvError2Str("av_dict_set", ret);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool size() const { return av_dict_count(dict); }
|
|
|
|
AVDictionary *dict = nullptr;
|
|
};
|
|
|
|
google::LogSeverity GlogLevelFromAvLevel(int avlevel) {
|
|
if (avlevel >= AV_LOG_INFO) {
|
|
return google::GLOG_INFO;
|
|
} else if (avlevel >= AV_LOG_WARNING) {
|
|
return google::GLOG_WARNING;
|
|
} else if (avlevel > AV_LOG_PANIC) {
|
|
return google::GLOG_ERROR;
|
|
} else {
|
|
return google::GLOG_FATAL;
|
|
}
|
|
}
|
|
|
|
void AvLogCallback(void *avcl, int avlevel, const char *fmt, va_list vl) {
|
|
if (avlevel > FLAGS_avlevel) {
|
|
return;
|
|
}
|
|
|
|
// google::LogMessage expects a "file" and "line" to be prefixed to the
|
|
// log message, like so:
|
|
//
|
|
// W1210 11:00:32.224936 28739 ffmpeg_rtsp:0] Estimating duration ...
|
|
// ^file ^line
|
|
//
|
|
// Normally this is filled in via the __FILE__ and __LINE__
|
|
// C preprocessor macros. In this case, try to fill in something useful
|
|
// based on the information ffmpeg supplies.
|
|
std::string file("ffmpeg");
|
|
if (avcl != nullptr) {
|
|
auto *avclass = *reinterpret_cast<AVClass **>(avcl);
|
|
file.push_back('_');
|
|
file.append(avclass->item_name(avcl));
|
|
}
|
|
char line[512];
|
|
vsnprintf(line, sizeof(line), fmt, vl);
|
|
google::LogSeverity glog_level = GlogLevelFromAvLevel(avlevel);
|
|
google::LogMessage(file.c_str(), 0, glog_level).stream() << line;
|
|
}
|
|
|
|
int AvLockCallback(void **mutex, enum AVLockOp op) {
|
|
auto typed_mutex = reinterpret_cast<std::mutex **>(mutex);
|
|
switch (op) {
|
|
case AV_LOCK_CREATE:
|
|
LOG_IF(DFATAL, *typed_mutex != nullptr)
|
|
<< "creating mutex over existing value.";
|
|
*typed_mutex = new std::mutex;
|
|
break;
|
|
case AV_LOCK_DESTROY:
|
|
delete *typed_mutex;
|
|
*typed_mutex = nullptr;
|
|
break;
|
|
case AV_LOCK_OBTAIN:
|
|
(*typed_mutex)->lock();
|
|
break;
|
|
case AV_LOCK_RELEASE:
|
|
(*typed_mutex)->unlock();
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
std::string StringifyVersion(int version_int) {
|
|
return StrCat((version_int >> 16) & 0xFF, ".", (version_int >> 8) & 0xFF, ".",
|
|
(version_int)&0xFF);
|
|
}
|
|
|
|
void LogVersion(const char *library_name, int compiled_version,
|
|
int running_version, const char *configuration) {
|
|
LOG(INFO) << library_name << ": compiled with version "
|
|
<< StringifyVersion(compiled_version) << ", running with version "
|
|
<< StringifyVersion(running_version)
|
|
<< ", configuration: " << configuration;
|
|
}
|
|
|
|
class RealInputVideoPacketStream : public InputVideoPacketStream {
|
|
public:
|
|
RealInputVideoPacketStream() {
|
|
ctx_ = CHECK_NOTNULL(avformat_alloc_context());
|
|
}
|
|
|
|
RealInputVideoPacketStream(const RealInputVideoPacketStream &) = delete;
|
|
RealInputVideoPacketStream &operator=(const RealInputVideoPacketStream &) =
|
|
delete;
|
|
|
|
~RealInputVideoPacketStream() final {
|
|
avformat_close_input(&ctx_);
|
|
avformat_free_context(ctx_);
|
|
}
|
|
|
|
bool GetNext(VideoPacket *pkt, std::string *error_message) final {
|
|
while (true) {
|
|
av_packet_unref(pkt->pkt());
|
|
int ret = av_read_frame(ctx_, pkt->pkt());
|
|
if (ret != 0) {
|
|
if (ret == AVERROR_EOF) {
|
|
error_message->clear();
|
|
} else {
|
|
*error_message = AvError2Str("av_read_frame", ret);
|
|
}
|
|
return false;
|
|
}
|
|
if (pkt->pkt()->stream_index != stream_index_) {
|
|
VLOG(3) << "Ignoring packet for stream " << pkt->pkt()->stream_index
|
|
<< "; only interested in " << stream_index_;
|
|
continue;
|
|
}
|
|
VLOG(2) << "Read packet with pts=" << pkt->pkt()->pts
|
|
<< ", dts=" << pkt->pkt()->dts << ", key=" << pkt->is_key();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const AVStream *stream() const final { return ctx_->streams[stream_index_]; }
|
|
|
|
private:
|
|
friend class RealVideoSource;
|
|
int64_t min_next_pts_ = std::numeric_limits<int64_t>::min();
|
|
int64_t min_next_dts_ = std::numeric_limits<int64_t>::min();
|
|
AVFormatContext *ctx_ = nullptr; // owned.
|
|
int stream_index_ = -1;
|
|
};
|
|
|
|
class RealVideoSource : public VideoSource {
|
|
public:
|
|
RealVideoSource() {
|
|
CHECK_GE(0, av_lockmgr_register(&AvLockCallback));
|
|
av_log_set_callback(&AvLogCallback);
|
|
av_register_all();
|
|
avformat_network_init();
|
|
LogVersion("avutil", LIBAVUTIL_VERSION_INT, avutil_version(),
|
|
avutil_configuration());
|
|
LogVersion("avformat", LIBAVFORMAT_VERSION_INT, avformat_version(),
|
|
avformat_configuration());
|
|
LogVersion("avcodec", LIBAVCODEC_VERSION_INT, avcodec_version(),
|
|
avcodec_configuration());
|
|
}
|
|
|
|
std::unique_ptr<InputVideoPacketStream> OpenRtsp(
|
|
const std::string &url, std::string *error_message) final {
|
|
std::unique_ptr<InputVideoPacketStream> stream;
|
|
Dictionary open_options;
|
|
if (!open_options.Set("rtsp_transport", "tcp", error_message) ||
|
|
!open_options.Set("user-agent", "moonfire-nvr", error_message) ||
|
|
// 10-second socket timeout, in microseconds.
|
|
!open_options.Set("stimeout", "10000000", error_message)) {
|
|
return stream;
|
|
}
|
|
|
|
stream = OpenCommon(url, &open_options.dict, error_message);
|
|
if (stream == nullptr) {
|
|
return stream;
|
|
}
|
|
|
|
// Discard the first packet.
|
|
LOG(INFO) << "Discarding the first packet to work around "
|
|
"https://trac.ffmpeg.org/ticket/5018";
|
|
VideoPacket dummy;
|
|
if (!stream->GetNext(&dummy, error_message)) {
|
|
stream.reset();
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
std::unique_ptr<InputVideoPacketStream> OpenFile(
|
|
const std::string &filename, std::string *error_message) final {
|
|
AVDictionary *open_options = nullptr;
|
|
return OpenCommon(filename, &open_options, error_message);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<InputVideoPacketStream> OpenCommon(
|
|
const std::string &source, AVDictionary **dict,
|
|
std::string *error_message) {
|
|
std::unique_ptr<RealInputVideoPacketStream> stream(
|
|
new RealInputVideoPacketStream);
|
|
|
|
int ret = avformat_open_input(&stream->ctx_, source.c_str(), nullptr, dict);
|
|
if (ret != 0) {
|
|
*error_message = AvError2Str("avformat_open_input", ret);
|
|
return std::unique_ptr<InputVideoPacketStream>();
|
|
}
|
|
|
|
if (av_dict_count(*dict) != 0) {
|
|
std::vector<std::string> ignored;
|
|
AVDictionaryEntry *ent = nullptr;
|
|
while ((ent = av_dict_get(*dict, "", ent, AV_DICT_IGNORE_SUFFIX)) !=
|
|
nullptr) {
|
|
ignored.push_back(StrCat(ent->key, "=", ent->value));
|
|
}
|
|
LOG(WARNING) << "avformat_open_input ignored " << ignored.size()
|
|
<< " options: " << Join(ignored, ", ");
|
|
}
|
|
|
|
ret = avformat_find_stream_info(stream->ctx_, nullptr);
|
|
if (ret < 0) {
|
|
*error_message = AvError2Str("avformat_find_stream_info", ret);
|
|
return std::unique_ptr<InputVideoPacketStream>();
|
|
}
|
|
|
|
// Find the video stream.
|
|
for (unsigned int i = 0; i < stream->ctx_->nb_streams; ++i) {
|
|
if (stream->ctx_->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
|
|
VLOG(1) << "Video stream index is " << i;
|
|
stream->stream_index_ = i;
|
|
break;
|
|
}
|
|
}
|
|
if (stream->stream() == nullptr) {
|
|
*error_message = StrCat("no video stream");
|
|
return std::unique_ptr<InputVideoPacketStream>();
|
|
}
|
|
|
|
return std::unique_ptr<InputVideoPacketStream>(stream.release());
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
bool OutputVideoPacketStream::OpenFile(const std::string &filename,
|
|
const InputVideoPacketStream &input,
|
|
std::string *error_message) {
|
|
CHECK(ctx_ == nullptr) << ": already open.";
|
|
CHECK(stream_ == nullptr);
|
|
|
|
if ((ctx_ = avformat_alloc_context()) == nullptr) {
|
|
*error_message = "avformat_alloc_context failed.";
|
|
return false;
|
|
}
|
|
|
|
ctx_->oformat = av_guess_format(nullptr, filename.c_str(), nullptr);
|
|
if (ctx_->oformat == nullptr) {
|
|
*error_message =
|
|
StrCat("Can't find output format for filename: ", filename.c_str());
|
|
avformat_free_context(ctx_);
|
|
ctx_ = nullptr;
|
|
return false;
|
|
}
|
|
|
|
int ret = avio_open2(&ctx_->pb, filename.c_str(), AVIO_FLAG_WRITE, nullptr,
|
|
nullptr);
|
|
if (ret < 0) {
|
|
avformat_free_context(ctx_);
|
|
ctx_ = nullptr;
|
|
*error_message = AvError2Str("avio_open2", ret);
|
|
return false;
|
|
}
|
|
stream_ = avformat_new_stream(ctx_, input.stream()->codec->codec);
|
|
if (stream_ == nullptr) {
|
|
avformat_free_context(ctx_);
|
|
ctx_ = nullptr;
|
|
unlink(filename.c_str());
|
|
*error_message = AvError2Str("avformat_new_stream", ret);
|
|
return false;
|
|
}
|
|
stream_->time_base = input.stream()->time_base;
|
|
ret = avcodec_copy_context(stream_->codec, input.stream()->codec);
|
|
if (ret != 0) {
|
|
avformat_free_context(ctx_);
|
|
ctx_ = nullptr;
|
|
stream_ = nullptr;
|
|
unlink(filename.c_str());
|
|
*error_message = AvError2Str("avcodec_copy_context", ret);
|
|
return false;
|
|
}
|
|
stream_->codec->codec_tag = 0;
|
|
if ((ctx_->oformat->flags & AVFMT_GLOBALHEADER) != 0) {
|
|
stream_->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
|
}
|
|
|
|
ret = avformat_write_header(ctx_, nullptr);
|
|
if (ret != 0) {
|
|
avformat_free_context(ctx_);
|
|
ctx_ = nullptr;
|
|
stream_ = nullptr;
|
|
unlink(filename.c_str());
|
|
*error_message = AvError2Str("avformat_write_header", ret);
|
|
return false;
|
|
}
|
|
frames_written_ = 0;
|
|
key_frames_written_ = 0;
|
|
return true;
|
|
}
|
|
|
|
bool OutputVideoPacketStream::Write(VideoPacket *pkt,
|
|
std::string *error_message) {
|
|
#if 0
|
|
if (pkt->pkt()->pts < min_next_pts_ || pkt->pkt()->dts < min_next_dts_) {
|
|
*error_message = StrCat("refusing to write non-increasing pts/dts, pts=",
|
|
pkt->pkt()->pts, " vs min ", min_next_pts_, " dts=",
|
|
pkt->pkt()->dts, " vs min ", min_next_dts_);
|
|
return false;
|
|
}
|
|
min_next_pts_ = pkt->pkt()->pts + 1;
|
|
min_next_dts_ = pkt->pkt()->dts + 1;
|
|
#endif
|
|
VLOG(2) << "Writing packet with pts=" << pkt->pkt()->pts
|
|
<< " dts=" << pkt->pkt()->dts << ", key=" << pkt->is_key();
|
|
int ret = av_write_frame(ctx_, pkt->pkt());
|
|
if (ret < 0) {
|
|
*error_message = AvError2Str("av_write_frame", ret);
|
|
return false;
|
|
}
|
|
++frames_written_;
|
|
if (pkt->is_key()) {
|
|
key_frames_written_++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void OutputVideoPacketStream::Close() {
|
|
if (ctx_ == nullptr) {
|
|
CHECK(stream_ == nullptr);
|
|
return;
|
|
}
|
|
|
|
int ret = av_write_trailer(ctx_);
|
|
if (ret != 0) {
|
|
LOG(WARNING) << AvError2Str("av_write_trailer", ret);
|
|
}
|
|
|
|
ret = avio_closep(&ctx_->pb);
|
|
if (ret != 0) {
|
|
LOG(WARNING) << AvError2Str("avio_closep", ret);
|
|
}
|
|
avformat_free_context(ctx_);
|
|
ctx_ = nullptr;
|
|
stream_ = nullptr;
|
|
frames_written_ = -1;
|
|
key_frames_written_ = -1;
|
|
min_next_pts_ = std::numeric_limits<int64_t>::min();
|
|
min_next_dts_ = std::numeric_limits<int64_t>::min();
|
|
}
|
|
|
|
VideoSource *GetRealVideoSource() {
|
|
static auto *real_video_source = new RealVideoSource; // never deleted.
|
|
return real_video_source;
|
|
}
|
|
|
|
} // namespace moonfire_nvr
|