Qt调用FFmpeg库实时播放UDP组播视频流
基于以下参考链接,通过改进实现实时播放UDP组播视频流
https://blog.csdn.net/u012532263/article/details/102736700
源码在windows(qt-opensource-windows-x86-5.12.9.exe)、ubuntu20.04.6(x64)(qt-opensource-linux-x64-5.12.12.run)、以及针对arm64的ubuntu20.04.6(x64)交叉编译环境下编译成功(QT5.12.8, 5.15.13), 可执行程序在windows,ubuntu(x64)、arm64上均可运行。
工程代码见:
https://download.csdn.net/download/daqinzl/90315016
主要代码
videoplayer.cpp
#include "videoplayer.h"
#include <QDebug>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/pixfmt.h"
#include "libswscale/swscale.h"
}
#include <stdio.h>
#include<iostream>
using namespace std;
VideoPlayer::VideoPlayer()
{
}
VideoPlayer::~VideoPlayer()
{
}
void VideoPlayer::startPlay()
{
///调用 QThread 的start函数 将会自动执行下面的run函数 run函数是一个新的线程
this->start();
}
void VideoPlayer::run()
{
/*
AVFormatContext *pFormatCtx;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVFrame *pFrame;
AVFrame *pFrameRGB;
AVPacket *packet;
uint8_t *out_buffer;
static struct SwsContext *img_convert_ctx;
int videoStream, i, numBytes;
int ret, got_picture;
avformat_network_init();
av_register_all();
//Allocate an AVFormatContext.
pFormatCtx = avformat_alloc_context();
// ffmpeg取rtsp流时av_read_frame阻塞的解决办法 设置参数优化
AVDictionary* avdic = NULL;
//rtsp
//av_dict_set(&avdic, "buffer_size", "102400", 0); //设置缓存大小,1080p可将值调大
//av_dict_set(&avdic, "rtsp_transport", "udp", 0); //以udp方式打开,如果以tcp方式打开将udp替换为tcp
//rtmp
//av_dict_set(&avdic, "buffer_size", "8192000", 0); //设置缓存大小,1080p可将值调大
//av_dict_set(&avdic, "rtsp_transport", "tcp", 0); //以udp方式打开,如果以tcp方式打开将udp替换为tcp
//udp
av_dict_set(&avdic, "buffer_size", "8192000", 0); //设置缓存大小,1080p可将值调大
av_dict_set(&avdic, "rtsp_transport", "udp", 0); //以udp方式打开,如果以tcp方式打开将udp替换为tcp
//av_dict_set(&avdic, "fflags", "nobuffer", 0); // 设置实时选项
//av_dict_set(&avdic, "flags", "low_delay", 0); // 设置低延迟选项
av_dict_set(&avdic, "max_interleave_delta", "40000", 0);
//av_dict_set(&avdic, "stimeout", "2000000", 0); //设置超时断开连接时间,单位微秒
//av_dict_set(&avdic, "max_delay", "500000", 0); //设置最大时延
///rtsp地址,可根据实际情况修改
//char url[]="rtsp://admin:admin@192.168.1.18:554/h264/ch1/main/av_stream";
//char url[]="rtsp://192.168.17.112/test2.264";
//char url[]="rtsp://admin:admin@192.168.43.1/stream/main";
//char url[]="rtmp://mobliestream.c3tv.com:554/live/goodtv.sdp";
char url[]="udp://224.1.1.1:5001";
//char url[]="rtmp://192.168.1.100:1935/live/desktop";
if (avformat_open_input(&pFormatCtx, url, NULL, &avdic) != 0) {
qDebug("can't open the file. \n");
return;
}
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
qDebug("Could't find stream infomation.\n");
return;
}
videoStream = -1;
///循环查找视频中包含的流信息,直到找到视频类型的流
///便将其记录下来 保存到videoStream变量中
///这里我们现在只处理视频流 音频流先不管他
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStream = i;
}
}
///如果videoStream为-1 说明没有找到视频流
if (videoStream == -1) {
qDebug("Didn't find a video stream.\n");
return;
}
///查找解码器
pCodecCtx = pFormatCtx->streams[videoStream]->codec;
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
///2017.8.9---lizhen
//pCodecCtx->bit_rate = 0; //初始化为0
//pCodecCtx->time_base.num = 1; //下面两行:一秒钟25帧
//pCodecCtx->time_base.den = 10;
//pCodecCtx->frame_number = 1; //每包一个视频帧
if (pCodec == NULL) {
qDebug("Codec not found.\n");
return;
}
///打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
qDebug("Could not open codec.\n");
return;
}
pFrame = av_frame_alloc();
pFrameRGB = av_frame_alloc();
///这里我们改成了 将解码后的YUV数据转换成RGB32
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_RGB32, SWS_FAST_BILINEAR, NULL, NULL, NULL);
numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height);
out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
avpicture_fill((AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_RGB32,
pCodecCtx->width, pCodecCtx->height);
int y_size = pCodecCtx->width * pCodecCtx->height;
packet = (AVPacket *) malloc(sizeof(AVPacket)); //分配一个packet
av_new_packet(packet, y_size); //分配packet的数据
//*/
//bool CanRun = true;
//ffmpeg 初始化
// 初始化注册ffmpeg相关的编码器
av_register_all();
avcodec_register_all();
avformat_network_init();
//qDebug()<<"1 FFmpeg version info: { av_version_info() }";
qDebug()<<"2 FFmpeg version info: { " << av_version_info() << "}";
qDebug()<<"3 FFmpeg version info:";
qDebug()<< av_version_info() ;
char url[]="udp://224.1.1.1:5001";
// ffmpeg 转码
// 分配音视频格式上下文
AVFormatContext *pFormatCtx = avformat_alloc_context();
AVDictionary* avdic = NULL;
av_dict_set(&avdic, "buffer_size", "8192000", 0);
av_dict_set(&avdic, "max_interleave_delta", "40000", 0);
av_dict_set(&avdic, "analyzeduration", "100000000", 0);
av_dict_set(&avdic, "probesize", "100000000", 0);
int ret;
//打开流
ret = avformat_open_input(&pFormatCtx, url, NULL, &avdic);
if (ret != 0){ qDebug("can't open the url. \n"); return; }
// 读取媒体流信息
ret = avformat_find_stream_info(pFormatCtx, NULL);
if (ret != 0) { qDebug("Could't find stream infomation.\n"); return; }
// 这里只是为了打印些视频参数
AVDictionaryEntry* tag = NULL;
while ((tag = av_dict_get(pFormatCtx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)) != NULL)
{
char * key = tag->key;
char * value = tag->value;
qDebug()<< *key << *value << "\n";
}
// 从格式化上下文获取流索引
AVStream* pStream = NULL;
//AVStream* aStream = NULL;
int videoStream = -1;
for (int i = 0; i < pFormatCtx->nb_streams; i++)
{
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
pStream = pFormatCtx->streams[i];
videoStream = i;
}
//else if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
//{
// aStream = pFormatCtx->streams[i];
//}
}
if (pStream == NULL) { qDebug("Didn't find a video stream.\n"); return; }
// 获取流的编码器上下文
AVCodecContext codecContext = *pStream->codec;
qDebug() << "codec name: {" << avcodec_get_name(codecContext.codec_id) << "}\n";
// 获取图像的宽、高及像素格式
int width = codecContext.width;
int height = codecContext.height;
AVPixelFormat sourcePixFmt = codecContext.pix_fmt;
// 得到编码器ID
AVCodecID codecId = codecContext.codec_id;
// 目标像素格式
AVPixelFormat destinationPixFmt = AV_PIX_FMT_RGB32;
// 某些264格式codecContext.pix_fmt获取到的格式是AV_PIX_FMT_NONE 统一都认为是YUV420P
if (sourcePixFmt == AV_PIX_FMT_NONE && codecId == AV_CODEC_ID_MPEG2TS)
{
sourcePixFmt = AV_PIX_FMT_YUV420P;
}
static struct SwsContext *img_convert_ctx;
// 得到SwsContext对象:用于图像的缩放和转换操作
img_convert_ctx = sws_getContext(width, height, sourcePixFmt,
width, height, destinationPixFmt,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
if (img_convert_ctx == NULL) { qDebug() << "Could not initialize the conversion context.\n" ; return; }
//分配一个默认的帧对象:AVFrame
//AVFrame* pConvertedFrame = av_frame_alloc();
// 目标媒体格式需要的字节长度
//numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height);
int numBytes = avpicture_get_size(destinationPixFmt, width, height);
// 分配目标媒体格式内存使用
//out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
uint8_t * out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
//var dstData = new byte_ptrArray4();
//var dstLinesize = new int_array4();
AVFrame *pFrameRGB = av_frame_alloc();
// 设置图像填充参数
//av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, convertedFrameBufferPtr, destinationPixFmt, width, height, 1);
avpicture_fill((AVPicture *) pFrameRGB, out_buffer, destinationPixFmt,width, height);
// ffmpeg 解码
// 根据编码器ID获取对应的解码器
AVCodec* pCodec = avcodec_find_decoder(codecId);
if (pCodec == NULL) { qDebug() << "Unsupported codec.\n"; }
AVCodecContext* pCodecCtx = &codecContext;
if ((pCodec->capabilities & AV_CODEC_CAP_TRUNCATED) == AV_CODEC_CAP_TRUNCATED)
pCodecCtx->flags |= AV_CODEC_FLAG_TRUNCATED;
// 通过解码器打开解码器上下文:AVCodecContext pCodecContext
ret = avcodec_open2(pCodecCtx, pCodec, NULL);
if (ret < 0) { qDebug() << ret << "\n"; return; }
// 分配解码帧对象:AVFrame pDecodedFrame
AVFrame* pFrame = av_frame_alloc();
// 初始化媒体数据包
AVPacket* packet = new AVPacket();
AVPacket** pPacket = &packet;
av_init_packet(packet);
while (1)
{
/*
if (av_read_frame(pFormatCtx, packet) < 0)
{
qDebug() << "av_read_frame < 0";
break; //这里认为视频读取完了
}
if (packet->stream_index == videoStream) {
ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);
if (ret < 0) {
qDebug("decode error.\n");
return;
}
if (got_picture) {
sws_scale(img_convert_ctx,
(uint8_t const * const *) pFrame->data,
pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
pFrameRGB->linesize);
//把这个RGB数据 用QImage加载
QImage tmpImg((uchar *)out_buffer,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32);
QImage image = tmpImg.copy(); //把图像复制一份 传递给界面显示
emit sig_GetOneFrame(image); //发送信号
//emit sig_GetOneFrame(tmpImg);
//提取出图像中的R数据
//for(int i=0;i<pCodecCtx->width;i++)
//{
// for(int j=0;j<pCodecCtx->height;j++)
// {
// QRgb rgb=image.pixel(i,j);
// int r=qRed(rgb);
// image.setPixel(i,j,qRgb(r,0,0));
// }
//}
//emit sig_GetRFrame(image);
}else qDebug() << "got_picture < 0";
}else qDebug() << "packet->stream_index not video stream";
av_free_packet(packet); //释放资源,否则内存会一直上升
//msleep(0.02); //停一停 不然放的太快了
//*/
//*
try
{
do
{
// 读取一帧未解码数据
ret = av_read_frame(pFormatCtx, packet);
// Console.WriteLine(pPacket->dts);
if (ret == AVERROR_EOF) break;
if (ret < 0) { qDebug() << "got error "; return; }
if (packet->stream_index != videoStream) continue;
// 解码
ret = avcodec_send_packet(pCodecCtx, packet);
if (ret < 0) { qDebug() << "got error 2 "; return; }
// 解码输出解码数据
ret = avcodec_receive_frame(pCodecCtx, pFrame);
} while (ret == AVERROR(EAGAIN) && 1);
if (ret == AVERROR_EOF) break;
if (ret < 0) { qDebug() << "got error 3 "; return; }
if (packet->stream_index != videoStream) continue;
//Console.WriteLine($@"frame: {frameNumber}");
// YUV->RGB
sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
//把这个RGB数据 用QImage加载
QImage tmpImg((uchar *)out_buffer,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32);
QImage image = tmpImg.copy(); //把图像复制一份 传递给界面显示
emit sig_GetOneFrame(image); //发送信号
//emit sig_GetOneFrame(tmpImg);
} catch (exception e) {
}
{
av_packet_unref(packet);//释放数据包对象引用
av_frame_unref(pFrame);//释放解码帧对象引用
}
//*/
}
av_free(out_buffer);
av_free(pFrameRGB);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
}
相关文章:
Qt调用FFmpeg库实时播放UDP组播视频流
基于以下参考链接,通过改进实现实时播放UDP组播视频流 https://blog.csdn.net/u012532263/article/details/102736700 源码在windows(qt-opensource-windows-x86-5.12.9.exe)、ubuntu20.04.6(x64)(qt-opensource-linux-x64-5.12.12.run)、以…...
C# 类与对象详解
.NET学习资料 .NET学习资料 .NET学习资料 在 C# 编程中,类与对象是面向对象编程的核心概念。它们让开发者能够将数据和操作数据的方法封装在一起,从而构建出模块化、可维护且易于扩展的程序。下面将详细介绍 C# 中类与对象的相关知识。 一、类的定义 …...
【Elasticsearch 基础入门】Centos7下Elasticsearch 7.x安装与配置(单机)
Elasticsearch系列文章目录 【Elasticsearch 基础入门】一文带你了解Elasticsearch!!!【Elasticsearch 基础入门】Centos7下Elasticsearch 7.x安装与配置(单机) 目录 Elasticsearch系列文章目录前言单机模式1. 安装 J…...
大模型本地部署使用方法(Ollama脚手架工具、FisherAI浏览器大模型插件、AnythingLLM大模型集成应用平台)
一、Ollama (一)Ollama简介 Ollama是一个专为在本地环境中运行和定制大型语言模型而设计的工具。它提供简单高效的接口,用于创建、运行和管理这些模型,方便用户直接使用,也方便用作后台服务支撑其它应用程序。熟悉网…...
【华为OD-E卷 - 报数游戏 100分(python、java、c++、js、c)】
【华为OD-E卷 - 报数游戏 100分(python、java、c、js、c)】 题目 100个人围成一圈,每个人有一个编码,编号从1开始到100。 他们从1开始依次报数,报到为M的人自动退出圈圈,然后下一个人接着从1开始报数&…...
深入理解Spring框架:从基础到实践
前言 Spring框架是一个开源的企业级应用开发框架,它为Java开发者提供了灵活的架构支持,特别是在依赖注入(IOC)和面向切面编程(AOP)方面。本文将通过具体的示例,带你从Spring框架的概述、IOC容器…...
一觉醒来全球编码能力下降100000倍,新手小白的我决定科普C语言——函数
1. 函数的概念 数学中我们其实就⻅过函数的概念,⽐如:⼀次函数 y kx b ,k和b都是常数,给⼀个任意的 x,就得到⼀个y值。其实在C语⾔也引⼊函数(function)的概念,有些翻译为…...
CentOS 上安装 Go (Golang)
1. 检查系统环境 确保系统为 CentOS 7 或 CentOS 8,或者其他兼容的 Linux 发行版。 cat /etc/os-release2. 安装依赖 安装一些必要的工具: sudo yum update -y sudo yum install -y wget tar3. 下载 Go 从 Go 官方下载页面获取适用于 Linux 的最新版…...
软件模拟I2C案例前提须知——EEPROM芯片之M24C02
引言 了解了I2C的基础知识后,我们将来使用一个I2C案例实践来深入理解I2C通讯,即软件模拟I2C。顾名思义,就是利用软件方式通过模拟I2C协议要求的时序或者说一些相关规定来实现一个I2C通讯协议,然后利用模拟出的I2C协议来实现两个设…...
GIS教程:全国数码商城系统
文章目录 注册高德地图API普通网页中测试地图加载地图添加标记地图配置点标记 Marker添加弹框创建vue项目并添加高德地图创建项目加载高德地图项目首页布局封装axios和配置代理服务器获取城市热门信息获取城市区县信息获取区县商城信息获取指定城市区县的经纬度坐标将地图缩放到…...
BroadCom-RDMA博通网卡如何进行驱动安装和设置使得对应网口具有RDMA功能以适配RDMA相机
BroadCom-RDMA博通网卡如何进行驱动安装和设置使得对应网口具有RDMA功能以适配RDMA相机 BroadCom-RDMA 博通网卡Baumer-RDMA 万兆网相机Baumer工业相机RDMA功能的技术背景BroadCom-RDMA博通网卡如何进行驱动安装和设置具有RDMA功能一、安装ZVA-BroadCom-RDMA网卡二、设备管理器…...
分布式微服务系统架构第90集:现代化金融核心系统
#1.1 深化数字化转型,核心面临新挑战 1、架构侧:无法敏捷协同数字金融经营模式转型。 2、需求侧:业务需求传导低效始终困扰金融机构。 3、开发侧:创新产品上市速度低于期望。 4、运维侧:传统面向资源型监控体系难以支撑…...
进阶数据结构——双向循环链表
目录 前言一、定义与结构二、特点与优势三、基本操作四、应用场景五、实现复杂度六、动态图解七、代码模版(c)八、经典例题九、总结结语 前言 这一期我们学习双向循环链表。双向循环链表不同于单链表,双向循环链表是一种特殊的数据结构&…...
记录一次,PyQT的报错,多线程Udp失效,使用工具如netstat来检查端口使用情况。
1.问题 报错Exception in thread Thread-1: Traceback (most recent call last): File "threading.py", line 932, in _bootstrap_inner File "threading.py", line 870, in run File "main.py", line 456, in udp_recv IndexError: list…...
安装anaconda3 后 电脑如何单独运行python,python还需要独立安装吗?
安装anaconda3 后 电脑如何单独运行python,python还需要独立安装吗? 电脑第一此安装anaconda用于jupyter notebook使用。 但是在运行cmd的时候,输入python --version 显示未安装或跳转商店提示安装。 明明我可以运行python但是为什么cmd却说我没安装呢…...
电子电气架构 --- 汽车电子拓扑架构的演进过程
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活…...
ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务
目录 一、ASP.NET Core 中使用依赖注入 (DI) 容器获取并执行自定义服务 1. app.Services 2. GetRequiredService() 3. Init() 二、应用场景 三、依赖注入使用拓展 1、使用场景 2、使用步骤 1. 定义服务接口和实现类 2. 注册服务到依赖注入容器 3. 使用依赖注入获取并…...
leetcode——验证二叉搜索树(java)
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下: 节点的左子树只包含小于当前节点的数。 节点的右子树只包含 大于 当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。 示例 1: 输入…...
搜索引擎快速收录:关键词布局的艺术
本文来自:百万收录网 原文链接:https://www.baiwanshoulu.com/21.html 搜索引擎快速收录中的关键词布局,是一项既精细又富有策略性的工作。以下是对关键词布局艺术的详细阐述: 一、关键词布局的重要性 关键词布局影响着后期页面…...
VLN视觉语言导航基础
0 概述 视觉语言导航模型旨在构建导航决策模型 π π π,在 t t t时刻,模型能够根据指令 W W W、历史轨迹 τ { V 1 , V 2 , . . . , V t − 1 } \tau\{V_1,V_2,...,V_{t-1}\} τ{V1,V2,...,Vt−1}和当前观察 V t { P t , R t , N ( V t ) } V_…...
告别选型纠结!一文看懂RK3588与RK3588S的五大核心差异,帮你选对核心板
RK3588与RK3588S深度对比:从芯片选型到产品落地的实战指南 在智能硬件开发领域,芯片选型往往决定了产品的性能上限和成本结构。面对Rockchip推出的两款旗舰级处理器RK3588和RK3588S,许多开发者都会陷入选择困难。这两款芯片看似同源ÿ…...
别再为LaTeX自定义命令报错发愁了!手把手教你玩转\newcommand和\renewcommand
LaTeX自定义命令完全避坑指南:从报错到精通的实战手册 当你第一次在LaTeX文档中尝试自定义命令时,屏幕上突然跳出的红色报错信息往往让人手足无措。"Command already defined"、"Undefined control sequence"这些看似简单的错误提示…...
将 Claude Code 编程助手无缝对接至 Taotoken 服务的配置指南
将 Claude Code 编程助手无缝对接至 Taotoken 服务的配置指南 1. 准备工作 在开始配置之前,请确保已具备以下条件:已注册 Taotoken 账户并获取有效的 API Key,同时已在本地安装 Claude Code 编程助手。Taotoken 提供的 API Key 可在控制台的…...
别再死磕公差了!从乐高到汽车,聊聊四种装配方法如何帮你又快又好地搞定产品
别再死磕公差了!从乐高到汽车,聊聊四种装配方法如何帮你又快又好地搞定产品 装配精度是制造业永恒的话题,但很多人一提到装配就想到死磕公差,仿佛只有把每个零件的加工精度做到极致才能保证最终产品的质量。这种思维不仅成本高昂&…...
从一次真实的授权测试复盘:Fscan在内网横向移动中的实战技巧与参数调优
从一次真实的授权测试复盘:Fscan在内网横向移动中的实战技巧与参数调优 去年参与某金融企业的红队演练时,遇到一个典型的多层网络隔离环境。当我们通过钓鱼邮件拿下外围Web服务器后,发现内网存在大量ACL限制,传统扫描工具要么速度…...
移动端CV新宠:手把手教你用MobileViTv3在ImageNet上复现SOTA结果(附代码)
移动端视觉Transformer实战:MobileViTv3从环境配置到ImageNet复现全指南 在移动端视觉任务领域,传统CNN模型长期占据主导地位,而Transformer架构的崛起为轻量级模型带来了新的可能性。MobileViTv3作为该系列的最新迭代,通过创新的…...
轻量级Docker容器管理面板ClawPanel部署与安全配置指南
1. 项目概述与核心价值最近在折腾个人服务器和容器化应用时,发现一个挺普遍的需求:我们手头可能有好几个不同的服务,比如一个博客、一个图床、一个数据库管理面板,它们各自运行在不同的容器里。每次想看看哪个服务状态怎么样&…...
用C语言手搓一个2048游戏核心逻辑(附XTU-OJ 1239题解)
从零实现2048游戏核心逻辑:C语言算法精解与XTU-OJ 1239实战 在算法学习的道路上,将抽象规则转化为具体代码的能力至关重要。2048这款经典数字合并游戏,恰好提供了绝佳的算法训练场景。本文将带你从零开始,用C语言构建完整的游戏核…...
如何5分钟搞定智慧树刷课?终极自动化学习助手完全指南
如何5分钟搞定智慧树刷课?终极自动化学习助手完全指南 【免费下载链接】Autovisor 2025智慧树刷课脚本 基于Python Playwright的自动化程序 [有免安装版] 项目地址: https://gitcode.com/gh_mirrors/au/Autovisor 还在为每天重复点击视频、等待课程结束而烦恼…...
Pseudogen:如何用3步将Python代码转化为人人都能看懂的伪代码?
Pseudogen:如何用3步将Python代码转化为人人都能看懂的伪代码? 【免费下载链接】pseudogen A tool to automatically generate pseudo-code from source code. 项目地址: https://gitcode.com/gh_mirrors/ps/pseudogen 你是否曾面对复杂的Python代…...
