// This file is part of Moonfire NVR, a security camera digital video recorder. // Copyright (C) 2016 Scott Lamb // // 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 . // // ffmpeg.cc: See ffmpeg.h for description. #include "ffmpeg.h" #include extern "C" { #include #include #include #include #include #include } // extern "C" #include #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(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(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::min(); int64_t min_next_dts_ = std::numeric_limits::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 OpenRtsp( const std::string &url, std::string *error_message) final { std::unique_ptr 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 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 OpenCommon( const std::string &source, AVDictionary **dict, std::string *error_message) { std::unique_ptr 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(); } if (av_dict_count(*dict) != 0) { std::vector 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(); } // 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(); } return std::unique_ptr(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::min(); min_next_dts_ = std::numeric_limits::min(); } VideoSource *GetRealVideoSource() { static auto *real_video_source = new RealVideoSource; // never deleted. return real_video_source; } } // namespace moonfire_nvr