是什么?
- FFmpeg是一个开源的多媒体处理工具包,它集成了多种功能,包括音视频的录制、转换和流式传输处理。
- FFmpeg由一系列的库和工具组成,其中最核心的是libavcodec和libavformat库。
- libavcodec是一个领先的音频/视频编解码器库,支持多种音频和视频格式的编码和解码操作。
- libavformat库则用于处理各种不同的多媒体容器格式,如MP4、AVI、MKV等。它能够解析提取其中的音频和视频流,以便进行进一步的处理
为什么?
为什么有GStreamer的前提下,还要使用FFmpeg?
- 在某些情况下,GStreamer 会比 FFmpeg 更适合特定的需求
- FFmpeg 是一个功能强大的单体框架,主要专注于媒体的编解码、转码和流处理
- GStreamer适合构建复杂的媒体处理管道,如视频会议、实时流媒体处理和视频编辑
功能 | FFmpeg | GStreamer |
---|---|---|
核心架构 | Monolithic | 模块化,基于管道线 |
易用性 | 使用 CLI 更容易完成简单的媒体任务 | 需要更多设置,但管道灵活 |
编解码器支持 | 广泛的编解码器支持,几乎涵盖所有编解码器 | 功能强大,但通常需要额外的插件 |
使用案例 | 媒体转换、流媒体 | 转码、实时流媒体、媒体应用 |
模块化 | 模块化程度低,围绕特定命令构建 | 模块化程度高,可定制组件 |
插件系统 | 有限的插件系统,主要集中于编解码器 | 广泛的插件系统,用于自定义处理 |
实时处理 | 可以,但不直观 | 专为实时流媒体和处理而设计 |
跨平台 | 是(Linux、Windows、macOS 等) | 是(Linux、Windows、macOS 等) |
复杂性 | 对于简单的任务来说更简单 | 对于复杂的自定义媒体工作流程来说更好 |
许可 | LGPL 或 GPL(取决于配置) | LGPL |
开发者社区 | 庞大、活跃、被广泛采用 | 活跃,但规模小于 FFmpeg |
怎么做?
- 官网Download FFmpeg,选择安装包
Windows builds from gyan.dev
- 找到
release bulids
部分,选择 ffmpeg-release-essentials.zip - 解压文件并检查目录结构
- 配置环境变量 并检验 ffmpeg -version
核心本质
本质就四个名词
- Demuxer:拆解多媒体文件,提取音频和视频流。
- Decoder:将编码后的音频或视频数据解码为原始数据。
- Encoder:将原始音频或视频数据编码为特定格式。
- Muxer:将音频和视频流重新封装为多媒体文件。
![](https://i-blog.csdnimg.cn/direct/65bb27f65600437fbb907ac16328cf3f.png)
- Demuxer:拆解多媒体文件,提取音频和视频流。
![](https://i-blog.csdnimg.cn/direct/a582a4f591d04293a5edbb0d306565f3.png)
- 解码器接收音频、视频、 或字幕基本流,并将它们解码为原始帧( 视频的像素,音频的 PCM)
![](https://i-blog.csdnimg.cn/direct/643481f16d714e6aa9d85deb6b78c287.png)
- 编码器接收原始音频、视频或字幕帧并进行编码 它们被编码为数据包
![](https://i-blog.csdnimg.cn/direct/d6aa297d4b53487a8180560aeafae2ed.png)
- Muxer:将音频和视频流重新封装为多媒体文件
![](https://i-blog.csdnimg.cn/direct/e69dadc4e7c14a23a13a60135e989a81.png)
大局观总览
- 库总览
![](https://i-blog.csdnimg.cn/direct/8f917e4c04094abebf68e5bba190bc58.png)
真正有用三库
- libavcodec库包含多种音频、视频和字幕流的解码器和编码器,以及多种位流过滤器。
- libavformat库用于将音频、视频和字幕流多路复用和解复用
- libavfilter库用于处理音频/视频数据,例如进行视频的缩放、裁剪、旋转,音频的混音、音量调整
辅助类型库
-
libavutil包含了一些安全的、可移植的字符串函数、随机数生成器、数据结构、额外的数学函数、密码学和与多媒体相关的功能(如枚举像素和采样格式)
-
libswscale是一个用于图像缩放和颜色空间以及像素格式转换的高效库。
-
libswresample是一个高度优化的音频重采样、重矩阵和采样格式转换库。
-
libavdevice是一个通用的框架,用于抓取和渲染许多常见的多媒体输入/输出设备。
-
命令总览
![](https://i-blog.csdnimg.cn/direct/8515ccc39ce9424b93ffb0a0f106e8a3.png)
- 功能总览
![](https://i-blog.csdnimg.cn/direct/e30b499f6314428eabf5b275733cc908.png)
转换格式流程图
- 输入文件拆解多媒体文件,提取音频和视频流 ;将编码后的音频或视频数据解码为原始数据;将原始音频或视频数据编码为特定格式;将音频和视频流重新封装为多媒体文件;
![](https://i-blog.csdnimg.cn/direct/e036ee8ce82540c18b24552098ebbb02.png)
c
ffmpeg -i INPUT.mkv -map 0:v -map 0:a -c:v libx264 -c:a copy OUTPUT.mp4
简单的 filtergraph
- 将编码后的音频或视频数据解码为原始数据;并对原始数据进行处理;将原始音频或视频数据编码为特定格式;
![](https://i-blog.csdnimg.cn/direct/5404dca2d26349e0b64ce6cb15b92e55.png)
复杂 filtergraph 它可能有多个输入,多个输出,可能是不同类型的(音频或 video)
- 第二个输入的帧将叠加在来自第一个输入的帧上。第三个 input 被重新缩放,然后被复制到两个相同的流中。其中之一 它们叠加在组合的前两个输入上,显示为 FilterGraph 的第一个输出。另一个是 filterGraph 的第二个输出
![](https://i-blog.csdnimg.cn/direct/ca16773014ea49018d29e35e0e84f1d7.png)
基本命令
c
ffmpeg -y -c:a libfdk_aac -c:v libx264 -i input.mp4 -c:v libvpx-vp9 -c:a libvorbis output.webm
- -y:不经过确认,输出时直接覆盖同名文件
- -c:指定编码器
- -c copy:直接复制,不经过重新编码(这样比较快)
- -c:v:指定视频编码器
- -c:a:指定音频编码器
- -i:指定输入文件
- -an:去除音频流
- -vn: 去除视频流
- -preset:指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
- -f:fmt指定格式(音频或视频格式)
- -t:duration 记录时长为t
- -acodec: 音频选项, 一般后面加copy表示拷贝
- -vcodec:视频选项,一般后面加copy表示拷贝
- h264: 表示输出的是h264的视频裸流
- mp4: 表示输出的是mp4的视频
- mpegts: 表示ts视频流
c++
ffmpeg -r 1 -i racing.mp4 -c:v libx264 -profile:v high -level:v 5.1 -c:a copy -r 60 racingoutput.mp4
- -r 强制输入文件的帧速率(仅对 Raw 格式有效)为 1 fps,并且 输出文件的帧速率为 24 fps
- -profile:v -level 设置H.264画质级别
c
ffmpeg -i input.avi output.mp4
- 通过重新编码媒体流,将输入媒体文件转换为其他格式
c
ffmpeg -i input.avi -b:v 64k -bufsize 64k output.mp4
- -b:v 64k 设置输出文件的视频码率为 64 kbit/s
Streamcopy 流复制 -map 的使用
![](https://i-blog.csdnimg.cn/direct/68da05271d2541fb83f8832456457313.png)
c
ffmpeg -i INPUT.mkv -map 0:1 -c copy OUTPUT.mp4
![](https://i-blog.csdnimg.cn/direct/02eedce079344ad88aaee266dcc55b77.png)
c
ffmpeg -i INPUT0.mkv -i INPUT1.aac -map 0:0 -map 1:0 -c copy OUTPUT.mp4
![](https://i-blog.csdnimg.cn/direct/8d36c541c40f465ca82f4d6706c73aee.png)
c
ffmpeg -i INPUT.mkv -map 0:0 -c copy OUTPUT0.mp4 -map 0:1 -c copy OUTPUT1.mp4
推拉流RTSP命令
UDP推流
C
ffmpeg -re -i input.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream
- -re 为以流的方式读取
TCP推流
C
ffmpeg -re -i input.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://127.0.0.1:8554/stream
循环推流
C
ffmpeg -re -stream_loop -1 -i input.mp4 -c copy -f rtsp rtsp://127.0.0.1:8554/stream
- -stream_loop 为循环读取视频源的次数,-1为无限循环
拉流
c
ffplay rtsp://127.0.0.1:8554/stream
FFmpeg拉流保存成视频
c
ffmpeg -stimeout 30000000 -i rtsp://127.0.0.1:8554/stream -c copy output.mp4
- -stimeout 30000000 为等待RTSP 流连接的时间,单位为us微秒,等待 30 秒如果连接失败则退出
过滤器(fliter)命令
-
过滤器就是实现某一种视频功能的工具,FFmpeg自带开发了很多种filter用于实现不同的功能
-
源视频宽度扩大两倍
c
ffmpeg -i jidu.mp4 -t 10 -vf pad=2 * iw output.mp4
- 源视频水平翻转
c
ffmpeg -i jidu.mp4 -t 10 -vf hflip output2.mp4
- 水平翻转视频覆盖output.mp4
c
ffmpeg -i output.mp4 -i output2.mp4 -filter_complex overlay=w compare.mp4
核心结构体
- 围绕解协议,解封装,解码
- libavcodec 的核心是 AVCodec 和 AVCodecContext
AVIOContext,URLProtocol,URLContext,AVFormatContext
解协议(http,rtsp,rtmp,mms)
- AVIOContext,URLProtocol,URLContext
- AVIOContext是FFMPEG管理输入输出数据的结构体,内含指针指向URLContext结构体
- URLContext结构体中包含结构体URLProtocol
- URLContext存储音频/视频使用的协议的类型以及状态
- URLProtocol存储音频/视频使用的协议(rtp,rtmp,file等)操作函数接口
解封装(flv,avi,rmvb,mp4)
- AVFormatContext存储音频/视频封装包含的数据和信息
- AVInputFormat表示音频/视频输入格式
![](https://i-blog.csdnimg.cn/direct/0664901725054a6ea549f0f8d7a409fa.png)
AVCodec AVCodecContext AVStream
架构类似于昇腾的device conent steam
![](https://i-blog.csdnimg.cn/direct/1846153fc69d4352b59b1a4b415ffc0a.png)
解码(h264,mpeg2,aac,mp3)
- AVCodecContext,存储该音视频流使用解码器的相关数据(所需的上下文环境)
- AVCodec (该音视频的解码器)(h264 mpeg2 AAC mp3)
![](https://i-blog.csdnimg.cn/direct/84a01904e07645b4850b466486e02e5b.png)
- 实际使用时有可能会有多个 AVCodecContext 关联同一个 AVCodec 的情况。尤其是我们解码音频的时候。
音频文件时 5.1声道的 AVCodec AVCodecContext AVStream关系
-
通常会对应 3 个 AVStream
- 左右声道在一个 AVStream
- 环绕声在一个 AVStream
- 最后低音在另一个AVStream
-
3 个AVStream的编码可能是相同的
-
解码这个音频文件时就应该建立 3 个 AVCodecContext ,分别对应三个 AVStream。然后只需要有 1 个 AVCodec 。每个 AVCodecContext 都利用这一个 AVCodec 来解码。
AVPacket - AVFrame
- 所谓解码就是把一个 AVPacket 中的数据解成 AVFrame
- AVPacket是 编码压缩之后的数据
- AVFrame 是原始的,没有编码、没有压缩的数据 对视频来说是YUV RGB,对音频来说是PCM
![](https://i-blog.csdnimg.cn/direct/73a790eda72d440186ccf241ea4619d6.png)
I帧 P帧 B帧 的影响
-
我们会遇到前几个 AVPacket 解不出数据。
- 到了某个 AVPacket ,可以连续解出多个 AVFrame 来的情况。
- 这时这多个 AVFrame 就包括前面积压的 AVPacket 里的数据
-
avcodec_send_packet() 调用一次将一个 packet 推给Codec,
-
avcodec_receive_frame() 调用一次或多次来获得 frame
华为昇腾ACLLite库封装ffmpeg案例
什么是华为昇腾ACLLite库?
- ACLLite库是对CANN提供的ACL接口进行的高阶封装,简化用户调用流程,为用户提供一组简易的公共接口。当前主要针对边缘场景设计
![](https://i-blog.csdnimg.cn/direct/02d0594883ae4b2d89cddecb3f1dee25.png)
ACLLite\Media\CameraRead.cpp
c
void CameraRead::DecodeFrameThread(void* decoderSelf)
对照核心结构体 理解 具体解码流程
c++
void CameraRead::DecodeFrameThread(void* decoderSelf)
{
CameraRead* thisPtr = (CameraRead*)decoderSelf;
int videoStreamIndex = -1;
for (int i = 0; i < thisPtr->formatContext_->nb_streams; ++i) {
if (thisPtr->formatContext_->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex == -1) {
LOG_PRINT("[ERROR] usb camera %s index is -1", thisPtr->streamName_.c_str());
thisPtr->isOpened_ = false;
return;
}
AVCodecParameters* codecParameters = thisPtr->formatContext_->streams[videoStreamIndex]->codecpar;
AVCodec* codec = avcodec_find_decoder(codecParameters->codec_id);
if (codec == nullptr) {
LOG_PRINT("[ERROR] Could not find ffmpeg decoder.");
thisPtr->isOpened_ = false;
return;
}
AVCodecContext* codecContext = avcodec_alloc_context3(codec);
if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {
LOG_PRINT("[ERROR] Could not create decoder context.");
thisPtr->isOpened_ = false;
return;
}
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
LOG_PRINT("[ERROR] Could not open decoder context.");
thisPtr->isOpened_ = false;
return;
}
AVFrame* frame = av_frame_alloc();
AVPacket packet;
while (av_read_frame(thisPtr->formatContext_, &packet) >= 0 && !thisPtr->isStop_) {
if (packet.stream_index == videoStreamIndex) {
int response = avcodec_send_packet(codecContext, &packet);
if (response < 0 || response == AVERROR(EAGAIN)) {
continue;
}
while (response >= 0) {
response = avcodec_receive_frame(codecContext, frame);
if (response == AVERROR(EAGAIN)) {
break;
} else if (response < 0) {
LOG_PRINT("[ERROR] Receive false frame from ffmpeg.");
thisPtr->isOpened_ = false;
return;
}
bool ret = thisPtr->SendFrame(packet.data, packet.size);
if (!ret) {
thisPtr->isOpened_ = false;
LOG_PRINT("[ERROR] Send single frame from ffmpeg failed.");
return;
}
}
}
av_packet_unref(&packet);
}
av_frame_free(&frame);
avcodec_close(codecContext);
avformat_close_input(&thisPtr->formatContext_);
thisPtr->isOpened_ = false;
return;
}
- avcodec_find_decoder 根据编解码参数的编解码器ID查找对应的编解码器
- avcodec_alloc_context3 分配编解码器上下文
- avcodec_open2 打开编解码器上下文
- av_frame_alloc 分配帧结构体
- av_read_frame 从输入文件中读取一个数据包(AVPacket),并将其存储到 packet 中
- avcodec_send_packet 将一个数据包发送到解码器
- avcodec_receive_frame 从解码器中接收一个解码后的帧
- SendFrame 用于将解码后的帧发送到其他地方(例如显示或进一步处理)
- av_frame_free avcodec_close avformat_close_input 最后释放资源
ACLLite\DVPPLite\src\VideoRead.cpp
c
void FFmpegDecoder::Decode(FrameProcessCallBack callback,void *callbackParam)
对照核心结构体 理解 具体解码流程
c
void FFmpegDecoder::Decode(FrameProcessCallBack callback,
void *callbackParam)
{
LOG_PRINT("[INFO] Start ffmpeg decode video %s ...", streamName_.c_str());
avformat_network_init(); // init network
AVFormatContext* avFormatContext = avformat_alloc_context();
// check open video result
if (!OpenVideo(avFormatContext)) {
return;
}
int videoIndex = GetVideoIndex(avFormatContext);
if (videoIndex == kInvalidVideoIndex) { // check video index is valid
LOG_PRINT("[ERROR] Rtsp %s index is -1", streamName_.c_str());
return;
}
AVBSFContext* bsfCtx = nullptr;
// check initialize video parameters result
if (!InitVideoParams(videoIndex, avFormatContext, bsfCtx)) {
return;
}
LOG_PRINT("[INFO] Start decode frame of video %s ...", streamName_.c_str());
AVPacket avPacket;
int processOk = true;
// loop to get every frame from video stream
while ((av_read_frame(avFormatContext, &avPacket) == 0) && processOk && !isStop_) {
if (avPacket.stream_index == videoIndex) { // check current stream is video
// send video packet to ffmpeg
if (av_bsf_send_packet(bsfCtx, &avPacket)) {
LOG_PRINT("[ERROR] Fail to call av_bsf_send_packet, channel id:%s",
streamName_.c_str());
}
// receive single frame from ffmpeg
while ((av_bsf_receive_packet(bsfCtx, &avPacket) == 0) && !isStop_) {
int ret = callback(callbackParam, avPacket.data, avPacket.size);
if (ret != 0) {
processOk = false;
break;
}
}
}
av_packet_unref(&avPacket);
}
av_bsf_free(&bsfCtx); // free AVBSFContext pointer
avformat_close_input(&avFormatContext); // close input video
isFinished_ = true;
LOG_PRINT("[INFO] Ffmpeg decoder %s finished", streamName_.c_str());
}
- av_read_frame 从输入文件中读取一个数据包(AVPacket)
- av_bsf_send_packet 数据包发送到解码器
- av_bsf_receive_packet 从解码器中接收解码后的帧
- callback 进一步处理
- av_packet_unref av_bsf_free avformat_close_input 最后释放资源
根据对应类型初始化过滤器
c
void FFmpegDecoder::InitVideoStreamFilter(const AVBitStreamFilter*& videoFilter)
{
if (videoType_ == AV_CODEC_ID_H264) { // check video type is h264
videoFilter = av_bsf_get_by_name("h264_mp4toannexb"); // 目的是从Avcodec库中获取一个名为"h264_mp4toannexb"的视频过滤器。
} else { // the video type is h265
videoFilter = av_bsf_get_by_name("hevc_mp4toannexb");
}
}
附录-h264基础概念
H264
![](https://i-blog.csdnimg.cn/direct/083df9f5d9674a2a81f5acdc4ab13d2c.png)
为什么诞生
-
⼀段分辨率为 1920 * 1080,每个像素点为 RGB 占⽤3 个字节,帧率是 25 的视频,对于传输带宽的要求是
-
换成 bps 则意味着视频每秒带宽为 1186.523Mbps,这样的速率对于⽹络存储是不可接受的
-
H264 采⽤了 16 * 16 的分块⼤⼩对,视频帧图像进⾏相似⽐较和压缩编码
![](https://i-blog.csdnimg.cn/direct/ca0aba8a30c34e919cbff3a913012388.png)
- 一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了
![](https://i-blog.csdnimg.cn/direct/ded340b359d549769b634090f72835ad.png)
H264有两种封装
-
H.264码流分Annex-B和mp4两种格式。
-
⼀种是 annexb 模式,传统模式
-
⼀种是 mp4 模式,⼀般 mp4 mkv 都是 mp4 模式
-
很多解码器只⽀持 annexb 这种模式,因此需要将 mp4 做转换:
-
在 ffmpeg 中⽤h264_mp4toannexb_filter 可以做转换
c++
ffmpeg -i INPUT.mp4 -codec copy -bsf:v h264_mp4toannexb OUTPUT.ts
- -bsf:v h264_mp4toannexb:指定视频过滤器为h264_mp4toannexb,这个过滤器的作用是将H.264流从长度前缀模式转换为开始代码前缀模式。
H.264有四种画质级别,分别是baseline, extended, main, high:
- Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;
- Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;(用的少)
- Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced), 也支持CAVLC 和CABAC 的支持;
- High profile:高级画质。在main Profile 的基础上增加了8x8内部预测、自定义量化、 无损视频编码和更多的YUV 格式;
c++
ffmpeg -i input.mp4 -profile:v baseline -level 3.0 output.mp4
ffmpeg -i input.mp4 -profile:v main -level 4.2 output.mp4
ffmpeg -i input.mp4 -profile:v high -level 5.1 output.mp4
GOP - I帧 P帧 B帧
-
编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures )
-
解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示
-
GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成
-
I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)
简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。
![](https://i-blog.csdnimg.cn/direct/392d9e2554114aa383c8567eb54afc2d.png)
- I帧表示关键帧,你可以理解为经过适度地压缩这一帧画面的完整保留
- P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别
![](https://i-blog.csdnimg.cn/direct/3b398475cf224177bd6fc816aa7b3757.png)
- B帧记录的是本帧与前后帧的差别
- B帧传送的是它与前面的I帧或P帧和后面的P帧之间的预测误差及运动矢量
![](https://i-blog.csdnimg.cn/direct/94ce1564e2474ddd86fbc342f66bc908.png)
有了 I帧,P帧, 为什么需要B帧
- 因为B帧记录的是前后帧的差别,比P帧能节约更多的空间
IDR 图像(立即刷新图像)
- 一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像
- H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,
- 将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列
- 如果前一个序列出现重大错误,在这里可以获得重新同步的机会
- IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码
![](https://i-blog.csdnimg.cn/direct/e144de037f7b4cf3a06fc50be5714c2a.png)
DTS PTS
为什么会有PTS和DTS的概念
-
P帧需要参考前面的I帧或P帧才可以生成一张完整的图片
-
B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片
-
这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?
PTS和DTS的概念
- DTS(Decoding Time Stamp):即解码时间戳
- 这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
- PTS(Presentation Time Stamp):即显示时间戳
- 这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。
文档链接说明
-
参考文档
FFmpeg基础知识之------- H264编码profile & level控制_ffmpeg level-CSDN博客 -
参考文档
ffmpeg 结构体之间的关系_packet解码frame之间的对应-CSDN博客
FFMPEG结构体分析:AVIOContext-CSDN博客 -
参考文档
ffmpeg 常用命令汇总_ffmpeg命令大全-CSDN博客
基于FFmpeg进行rtsp推流及拉流(详细教程)_ffmpeg rtsp推流-CSDN博客 -
华为昇腾ACLLite仓库
Ascend/ACLLite -
华为昇腾对照概念
AscendCL架构及基本概念-AscendCL应用开发概述-AscendCL应用开发(C&C++)-应用开发-开发指南-CANN社区版8.0.0.alpha003开发文档-昇腾社区