accel.cpp 10.7 KB
Newer Older
1
/*
2
 *  Copyright (C) 2016-2019 Savoir-faire Linux Inc.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 *
 *  Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
 *
 *  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, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
 */

21 22 23
extern "C" {
#include <libavutil/hwcontext.h>
}
24

25
#include <algorithm>
26

27
#include "media_buffer.h"
28
#include "string_utils.h"
29
#include "fileutils.h"
30
#include "logger.h"
31 32
#include "accel.h"
#include "config.h"
33

Adrien Béraud's avatar
Adrien Béraud committed
34
namespace jami { namespace video {
35

36 37 38 39
struct HardwareAPI
{
    std::string name;
    AVPixelFormat format;
40
    AVPixelFormat swFormat;
41 42 43
    std::vector<AVCodecID> supportedCodecs;
};

44 45 46 47 48
static AVPixelFormat
getFormatCb(AVCodecContext* codecCtx, const AVPixelFormat* formats)
{
    auto accel = static_cast<HardwareAccel*>(codecCtx->opaque);

49 50 51
    AVPixelFormat fallback = AV_PIX_FMT_NONE;
    for (int i = 0; formats[i] != AV_PIX_FMT_NONE; ++i) {
        fallback = formats[i];
52 53
        if (accel && formats[i] == accel->getFormat()) {
            // found hardware format for codec with api
Adrien Béraud's avatar
Adrien Béraud committed
54
            JAMI_DBG() << "Found compatible hardware format for "
55
                << avcodec_get_name(static_cast<AVCodecID>(accel->getCodecId()))
56
                << " decoder with " << accel->getName();
57
            return formats[i];
58 59 60
        }
    }

Adrien Béraud's avatar
Adrien Béraud committed
61
    JAMI_WARN() << "Not using hardware decoding";
62
    return fallback;
63 64
}

65
HardwareAccel::HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format, AVPixelFormat swFormat, CodecType type)
66 67 68
    : id_(id)
    , name_(name)
    , format_(format)
69 70
    , swFormat_(swFormat)
    , type_(type)
71
{}
72

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
HardwareAccel::~HardwareAccel()
{
    if (deviceCtx_)
        av_buffer_unref(&deviceCtx_);
    if (framesCtx_)
        av_buffer_unref(&framesCtx_);
}

std::string
HardwareAccel::getCodecName() const
{
    if (type_ == CODEC_DECODER) {
        return avcodec_get_name(id_);
    } else if (type_ == CODEC_ENCODER) {
        std::stringstream ss;
        ss << avcodec_get_name(id_) << '_' << name_;
        return ss.str();
    }
    return "";
}

94
std::unique_ptr<VideoFrame>
95
HardwareAccel::transfer(const VideoFrame& frame)
96
{
97 98 99 100
    int ret = 0;
    if (type_ == CODEC_DECODER) {
        auto input = frame.pointer();
        if (input->format != format_) {
Adrien Béraud's avatar
Adrien Béraud committed
101
            JAMI_ERR() << "Frame format mismatch: expected "
102 103 104 105 106
                << av_get_pix_fmt_name(format_) << ", got "
                << av_get_pix_fmt_name(static_cast<AVPixelFormat>(input->format));
            return nullptr;
        }

107
        return transferToMainMemory(frame, swFormat_);
108 109 110
    } else if (type_ == CODEC_ENCODER) {
        auto input = frame.pointer();
        if (input->format != swFormat_) {
Adrien Béraud's avatar
Adrien Béraud committed
111
            JAMI_ERR() << "Frame format mismatch: expected "
112 113 114 115 116 117 118 119 120
                << av_get_pix_fmt_name(swFormat_) << ", got "
                << av_get_pix_fmt_name(static_cast<AVPixelFormat>(input->format));
            return nullptr;
        }

        auto framePtr = std::make_unique<VideoFrame>();
        auto hwFrame = framePtr->pointer();

        if ((ret = av_hwframe_get_buffer(framesCtx_, hwFrame, 0)) < 0) {
Adrien Béraud's avatar
Adrien Béraud committed
121
            JAMI_ERR() << "Failed to allocate hardware buffer: " << libav_utils::getError(ret);
122 123 124 125
            return nullptr;
        }

        if (!hwFrame->hw_frames_ctx) {
Adrien Béraud's avatar
Adrien Béraud committed
126
            JAMI_ERR() << "Failed to allocate hardware buffer: Cannot allocate memory";
127 128 129 130
            return nullptr;
        }

        if ((ret = av_hwframe_transfer_data(hwFrame, input, 0)) < 0) {
Adrien Béraud's avatar
Adrien Béraud committed
131
            JAMI_ERR() << "Failed to push frame to GPU: " << libav_utils::getError(ret);
132 133 134 135 136 137
            return nullptr;
        }

        hwFrame->pts = input->pts; // transfer does not copy timestamp
        return framePtr;
    } else {
Adrien Béraud's avatar
Adrien Béraud committed
138
        JAMI_ERR() << "Invalid hardware accelerator";
139
        return nullptr;
140
    }
141
}
142

143
void
144
HardwareAccel::setDetails(AVCodecContext* codecCtx)
145 146 147 148 149 150
{
    if (type_ == CODEC_DECODER) {
        codecCtx->hw_device_ctx = av_buffer_ref(deviceCtx_);
        codecCtx->get_format = getFormatCb;
        codecCtx->thread_safe_callbacks = 1;
    } else if (type_ == CODEC_ENCODER) {
151 152 153
        if (framesCtx_)
            // encoder doesn't need a device context, only a frame context
            codecCtx->hw_frames_ctx = av_buffer_ref(framesCtx_);
154
    }
155 156
}

157 158
bool
HardwareAccel::initDevice()
159
{
Philippe Gorley's avatar
Philippe Gorley committed
160
    int ret = 0;
161
    auto hwType = av_hwdevice_find_type_by_name(name_.c_str());
162 163 164
    if (hwType == AV_HWDEVICE_TYPE_NONE and (name_ == "nvenc" or name_ == "nvdec")) {
        hwType = AV_HWDEVICE_TYPE_CUDA;
    }
165 166
#ifdef HAVE_VAAPI_ACCEL_DRM
    // default DRM device may not work on multi GPU computers, so check all possible values
167
    if (name_ == "vaapi") {
168
        const std::string path = "/dev/dri/";
Adrien Béraud's avatar
Adrien Béraud committed
169
        auto files = jami::fileutils::readDirectory(path);
170 171 172 173
        // renderD* is preferred over card*
        std::sort(files.rbegin(), files.rend());
        for (auto& entry : files) {
            std::string deviceName = path + entry;
174 175
            if ((ret = av_hwdevice_ctx_create(&deviceCtx_, hwType, deviceName.c_str(), nullptr, 0)) >= 0) {
                return true;
176
            }
177 178
        }
    }
179 180
#endif
    // default device (nullptr) works for most cases
181 182 183 184 185 186 187 188 189
    ret = av_hwdevice_ctx_create(&deviceCtx_, hwType, nullptr, nullptr, 0);
    return ret >= 0;
}

bool
HardwareAccel::initFrame(int width, int height)
{
    int ret = 0;
    if (!deviceCtx_) {
Adrien Béraud's avatar
Adrien Béraud committed
190
        JAMI_ERR() << "Cannot initialize hardware frames without a valid hardware device";
191
        return false;
Philippe Gorley's avatar
Philippe Gorley committed
192
    }
193

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    framesCtx_ = av_hwframe_ctx_alloc(deviceCtx_);
    if (!framesCtx_)
        return false;

    auto ctx = reinterpret_cast<AVHWFramesContext*>(framesCtx_->data);
    ctx->format = format_;
    ctx->sw_format = swFormat_;
    ctx->width = width;
    ctx->height = height;
    ctx->initial_pool_size = 20; // TODO try other values

    if ((ret = av_hwframe_ctx_init(framesCtx_)) < 0)
        av_buffer_unref(&framesCtx_);

    return ret >= 0;
209 210
}

211 212 213 214 215 216 217 218 219 220 221 222 223
bool
HardwareAccel::linkHardware(AVBufferRef* framesCtx)
{
    if (framesCtx) {
        // Force sw_format to match swFormat_. Frame is never transferred to main
        // memory when hardware is linked, so the sw_format doesn't matter.
        auto hw = reinterpret_cast<AVHWFramesContext*>(framesCtx->data);
        hw->sw_format = swFormat_;

        if (framesCtx_)
            av_buffer_unref(&framesCtx_);
        framesCtx_ = av_buffer_ref(framesCtx);
        if ((linked_ = (framesCtx_ != nullptr))) {
Adrien Béraud's avatar
Adrien Béraud committed
224
            JAMI_DBG() << "Hardware transcoding pipeline successfully set up for"
225 226 227 228 229 230 231 232
                << " encoder '" << getCodecName() << "'";
        }
        return linked_;
    } else {
        return false;
    }
}

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
std::unique_ptr<VideoFrame>
HardwareAccel::transferToMainMemory(const VideoFrame& frame, AVPixelFormat desiredFormat)
{
    auto input = frame.pointer();
    auto out = std::make_unique<VideoFrame>();

    auto desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(input->format));
    if (desc && not (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
        out->copyFrom(frame);
        return out;
    }

    auto output = out->pointer();
    output->format = desiredFormat;

    int ret = av_hwframe_transfer_data(output, input, 0);
    if (ret < 0) {
        out->copyFrom(frame);
        return out;
    }

    output->pts = input->pts;
Adrien Béraud's avatar
Adrien Béraud committed
255 256
    if (AVFrameSideData* side_data = av_frame_get_side_data(input, AV_FRAME_DATA_DISPLAYMATRIX))
        av_frame_new_side_data_from_buf(output, AV_FRAME_DATA_DISPLAYMATRIX, av_buffer_ref(side_data->buf));
257 258 259 260
    return out;
}

std::unique_ptr<HardwareAccel>
261
HardwareAccel::setupDecoder(AVCodecID id, int width, int height)
262
{
263
    static const HardwareAPI apiList[] = {
264
        { "nvdec", AV_PIX_FMT_CUDA, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_H265, AV_CODEC_ID_VP8, AV_CODEC_ID_MJPEG } },
265 266 267
        { "vaapi", AV_PIX_FMT_VAAPI, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4, AV_CODEC_ID_VP8, AV_CODEC_ID_MJPEG } },
        { "vdpau", AV_PIX_FMT_VDPAU, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4 } },
        { "videotoolbox", AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4 } },
268 269
    };

270
    for (const auto& api : apiList) {
271 272
        if (std::find(api.supportedCodecs.begin(), api.supportedCodecs.end(), id) != api.supportedCodecs.end()) {
            auto accel = std::make_unique<HardwareAccel>(id, api.name, api.format, api.swFormat, CODEC_DECODER);
273 274 275 276
            if (accel->initDevice()) {
                 // we don't need frame context for videotoolbox
                if (api.format == AV_PIX_FMT_VIDEOTOOLBOX ||
                    accel->initFrame(width, height))  {
277
                    JAMI_DBG() << "Attempting to use hardware decoder " << accel->getCodecName() << " with " << api.name;
278 279
                    return accel;
                }
280 281 282 283 284 285 286 287
            }
        }
    }

    return nullptr;
}

std::unique_ptr<HardwareAccel>
288
HardwareAccel::setupEncoder(AVCodecID id, int width, int height, AVBufferRef* framesCtx)
289 290
{
    static const HardwareAPI apiList[] = {
291
        { "nvenc", AV_PIX_FMT_CUDA, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_H265 } },
292 293 294 295 296 297 298 299 300 301
        { "vaapi", AV_PIX_FMT_VAAPI, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MJPEG, AV_CODEC_ID_VP8 } },
        { "videotoolbox", AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264 } },
    };

    for (auto api : apiList) {
        const auto& it = std::find(api.supportedCodecs.begin(), api.supportedCodecs.end(), id);
        if (it != api.supportedCodecs.end()) {
            auto accel = std::make_unique<HardwareAccel>(id, api.name, api.format, api.swFormat, CODEC_ENCODER);
            const auto& codecName = accel->getCodecName();
            if (avcodec_find_encoder_by_name(codecName.c_str())) {
302 303 304 305 306
                if (accel->initDevice()) {
                    // we don't need frame context for videotoolbox
                    if (api.format == AV_PIX_FMT_VIDEOTOOLBOX ||
                        accel->linkHardware(framesCtx) ||
                        accel->initFrame(width, height)) {
307
                        JAMI_DBG() << "Attempting to use hardware encoder " << codecName << " with " << api.name;
308 309
                        return accel;
                    }
310
                }
311 312
            }
        }
313 314
    }

Adrien Béraud's avatar
Adrien Béraud committed
315
    JAMI_WARN() << "Not using hardware encoding";
316
    return nullptr;
317 318
}

Adrien Béraud's avatar
Adrien Béraud committed
319
}} // namespace jami::video