FFmpeg 实现从设备端获取音视频流并通过RTMP推流

news/2024/9/18 6:37:25/ 标签: ffmpeg, 音视频, h.264, aac

使用FFmpeg库(版本号为:4.4.2-0ubuntu0.22.04.1)实现从摄像头和麦克风获取音视频流并通过RTMP推流。
RTMP服务器使用的是SRS,我这边是跑在Ubuntu上的,最好是关闭掉系统防火墙,不然连接服务器好像会出问题,拉流端使用VLC。如果想要降低延时,请看我另外一篇博客,里面有说降低延时的方法。

代码如下:

#include <libavdevice/avdevice.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavutil/imgutils.h>
#include <pthread.h>
#include <libavutil/audio_fifo.h>typedef struct st_video
{enum AVPixelFormat camera_pix_fmt;AVStream *stream_out;AVFormatContext *context_in;AVFormatContext **context_out;int streamid;struct SwsContext *sws_ctx;AVCodecContext *codec_context;pthread_mutex_t *lock_write_frame;
} st_video;typedef struct st_audio
{AVStream *stream_out;AVFormatContext *context_in;AVFormatContext **context_out;int streamid;struct SwrContext *swr_ctx;AVCodecContext *codec_context;pthread_mutex_t *lock_write_frame;
} st_audio;int initVideo(st_video *s_video);
int initAudio(st_audio *s_audio);
void *thread_v(void *arg);
void *thread_a(void *arg);int main(void)
{int ret = -1;const char *url = "rtmp://192.168.3.230/live/livestream"; // rtmp地址AVFormatContext *context_out = NULL;pthread_mutex_t lock_write_frame;st_video s_video;s_video.context_out = &context_out;s_video.lock_write_frame = &lock_write_frame;st_audio s_audio;s_audio.context_out = &context_out;s_audio.lock_write_frame = &lock_write_frame;// 打印ffmpeg版本信息printf("ffmpeg version: %s\n", av_version_info());// 注册所有设备avdevice_register_all();// 分配输出格式上下文avformat_alloc_output_context2(&context_out, NULL, "flv", NULL);if (!context_out){printf("avformat_alloc_output_context2 failed\n");return -1;}// 初始化视频流initVideo(&s_video);// 初始化音频流initAudio(&s_audio);// 打开urlif (!(context_out->oformat->flags & AVFMT_NOFILE)){ret = avio_open(&context_out->pb, url, AVIO_FLAG_WRITE);if (ret < 0){printf("avio_open error (errmsg '%s')\n", av_err2str(ret));return -1;}}// 写入头部信息ret = avformat_write_header(context_out, NULL);if (ret < 0){avio_close(context_out->pb);printf("avformat_write_header failed\n");return -1;}pthread_t thread1, thread2;pthread_mutex_init(&lock_write_frame, NULL);pthread_create(&thread1, NULL, thread_v, &s_video);pthread_create(&thread2, NULL, thread_a, &s_audio);pthread_join(thread1, NULL);pthread_join(thread2, NULL);pthread_mutex_destroy(&lock_write_frame);if (s_video.sws_ctx)sws_freeContext(s_video.sws_ctx);if (s_video.context_in)avformat_close_input(&s_video.context_in);if (s_video.codec_context)avcodec_free_context(&s_video.codec_context);if (s_audio.codec_context)avcodec_free_context(&s_audio.codec_context);if (s_audio.swr_ctx)swr_free(&s_audio.swr_ctx);if (s_audio.context_in)avformat_close_input(&s_audio.context_in);if (context_out && !(context_out->flags & AVFMT_NOFILE))avio_close(context_out->pb);if (context_out)avformat_free_context(context_out);return 0;
}int initStream(AVFormatContext **context_in, enum AVMediaType type, int *streamid,const char *input_format_name, const char *device_name, AVDictionary **options,AVFormatContext *context_out, AVStream **stream_out)
{// 查找输入格式AVInputFormat *fmt = av_find_input_format(input_format_name);if (!fmt){printf("av_find_input_format error\n");return -1;}// 打开输入if (avformat_open_input(context_in, device_name, fmt, options) != 0){av_dict_free(options);printf("avformat_open_input error\n");return -1;}// 获取输入流信息if (avformat_find_stream_info(*context_in, NULL) < 0){printf("avformat_find_stream_info error\n");return -1;}// 获取流索引*streamid = av_find_best_stream(*context_in, type, -1, -1, NULL, 0);if (*streamid < 0){printf("cannot find video stream\n");return -1;}// 创建输出流*stream_out = avformat_new_stream(context_out, NULL);if (!(*stream_out)){avformat_free_context(context_out);printf("avformat_new_stream failed\n");return -1;}return 0;
}int initSws(struct SwsContext **sws_ctx, AVStream *stream_in, AVStream *stream_out)
{// 初始化转换上下文*sws_ctx = sws_getContext(stream_in->codecpar->width, stream_in->codecpar->height, stream_in->codecpar->format,stream_out->codecpar->width, stream_out->codecpar->height, stream_out->codecpar->format,SWS_BILINEAR, NULL, NULL, NULL);if (!sws_ctx){printf("sws_getContext error\n");return -1;}return 0;
}int initSwr(struct SwrContext **swr_ctx, AVStream *stream_in, AVStream *stream_out)
{// 根据通道数获取默认的通道布局,codecpar->channel_layou没有被设置,不能直接使用int64_t chlayout_in = av_get_default_channel_layout(stream_in->codecpar->channels);int64_t chlayout_out = av_get_default_channel_layout(stream_out->codecpar->channels);// 初始化重采样上下文*swr_ctx = swr_alloc_set_opts(NULL,chlayout_in, stream_out->codecpar->format, stream_out->codecpar->sample_rate,chlayout_out, stream_in->codecpar->format, stream_in->codecpar->sample_rate,0, NULL);if (!(*swr_ctx) || swr_init(*swr_ctx) < 0){printf("allocate resampler context failed\n");return -1;}return 0;
}int setVcodec(AVCodecContext **codec_context, AVStream *stream_in,int frame_rate, AVFormatContext *context_out, AVStream *stream_out)
{AVCodec *c = NULL;// 查找编码器c = avcodec_find_encoder(AV_CODEC_ID_H264);if (!c){printf("Codec not found\n");return -1;}printf("codec name: %s\n", c->name);// 分配编码器上下文*codec_context = avcodec_alloc_context3(c);if (!(*codec_context)){printf("avcodec_alloc_context3 failed\n");return -1;}AVCodecContext *ctx = *codec_context;// 设置编码器参数ctx->codec_id = AV_CODEC_ID_H264;ctx->codec_type = AVMEDIA_TYPE_VIDEO;ctx->pix_fmt = AV_PIX_FMT_YUV420P;ctx->width = stream_in->codecpar->width;ctx->height = stream_in->codecpar->height;ctx->time_base = (AVRational){1, frame_rate};         // 设置时间基ctx->framerate = (AVRational){frame_rate, 1};         // 设置帧率ctx->bit_rate = 750 * 1000;                           // 设置比特率ctx->gop_size = frame_rate;                           // 设置GOP大小ctx->max_b_frames = 0;                                // 设置最大B帧数,不需要B帧时设置为0av_opt_set(ctx->priv_data, "profile", "baseline", 0); // 设置h264画质级别av_opt_set(ctx->priv_data, "tune", "zerolatency", 0); // 设置h264编码优化参数// 检测输出上下文的封装格式,判断是否设置 AV_CODEC_FLAG_GLOBAL_HEADER// AV_CODEC_FLAG_GLOBAL_HEADER:由原来编码时在每个关键帧前加入pps和sps,改变为在extradate这个字节区加入pps和spsif (context_out->oformat->flags & AVFMT_GLOBALHEADER){printf("set AV_CODEC_FLAG_GLOBAL_HEADER\n");ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;}// 打开编码器if (avcodec_open2(ctx, c, NULL) < 0){avcodec_free_context(codec_context);printf("avcodec_open2 failed\n");return -1;}// 将编码器参数复制到流int ret = avcodec_parameters_from_context(stream_out->codecpar, ctx);if (ret < 0){avcodec_free_context(codec_context);printf("avcodec_parameters_from_context failed\n");return -1;}return 0;
}int setAcodec(AVCodecContext **codec_context, AVStream *stream_in,AVFormatContext *context_out, AVStream *stream_out)
{AVCodec *c = NULL;// 查找编码器c = avcodec_find_encoder(AV_CODEC_ID_AAC);if (!c){printf("Codec not found\n");return -1;}printf("codec name: %s\n", c->name);// 分配编码器上下文*codec_context = avcodec_alloc_context3(c);if (!c){printf("avcodec_alloc_context3 failed\n");return -1;}AVCodecContext *ctx = *codec_context;// 设置编码器参数ctx->codec_id = AV_CODEC_ID_AAC;ctx->codec_type = AVMEDIA_TYPE_AUDIO;ctx->sample_fmt = AV_SAMPLE_FMT_FLTP;ctx->sample_rate = stream_in->codecpar->sample_rate;ctx->channels = stream_in->codecpar->channels;ctx->channel_layout = av_get_default_channel_layout(stream_in->codecpar->channels);ctx->bit_rate = 64000;ctx->profile = FF_PROFILE_AAC_LOW;if (context_out->oformat->flags & AVFMT_GLOBALHEADER){printf("set AV_CODEC_FLAG_GLOBAL_HEADER\n");ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;}// 打开编码器if (avcodec_open2(ctx, c, NULL) < 0){avcodec_free_context(codec_context);printf("avcodec_open2 failed\n");return -1;}// 将编码器参数复制到流int ret = avcodec_parameters_from_context(stream_out->codecpar, ctx);if (ret < 0){avcodec_free_context(codec_context);printf("avcodec_parameters_from_context failed\n");return -1;}return 0;
}int initVideo(st_video *s_video)
{s_video->streamid = -1;const char *input_format_name = "video4linux2"; // 输入格式名称,Linux下为video4linux2或v4l2const char *device_name = "/dev/video0";        // 摄像头设备名称const char *camera_resolution = "640x480";      // 摄像头分辨率s_video->camera_pix_fmt = AV_PIX_FMT_YUYV422;   // 摄像头像素格式int frame_rate = 25;                            // 帧率int ret = -1;AVDictionary *options = NULL;av_dict_set(&options, "video_size", camera_resolution, 0); // 设置分辨率ret = initStream(&s_video->context_in, AVMEDIA_TYPE_VIDEO, &s_video->streamid,input_format_name, device_name, &options,*(s_video->context_out), &s_video->stream_out);if (ret < 0){printf("initStream failed\n");return -1;}AVStream *stream_v = s_video->context_in->streams[s_video->streamid];printf("video stream, width: %d, height: %d, format: %s\n",stream_v->codecpar->width, stream_v->codecpar->height,av_get_pix_fmt_name((enum AVPixelFormat)stream_v->codecpar->format));ret = setVcodec(&s_video->codec_context, stream_v, frame_rate,*(s_video->context_out), s_video->stream_out);if (ret < 0){printf("setVcodec failed\n");return -1;}ret = initSws(&s_video->sws_ctx, stream_v, s_video->stream_out);if (ret < 0){printf("initSws failed\n");return -1;}return 0;
}int initAudio(st_audio *s_audio)
{const char *input_format_name = "alsa";const char *device_name = "hw:1,0";   // 麦克风设备名称const char *in_sample_rate = "16000"; // 采样率const char *in_channels = "1";        // 声道数int ret = -1;AVDictionary *options = NULL;// 设置麦克风音频参数av_dict_set(&options, "sample_rate", in_sample_rate, 0);av_dict_set(&options, "channels", in_channels, 0);ret = initStream(&s_audio->context_in, AVMEDIA_TYPE_AUDIO, &s_audio->streamid,input_format_name, device_name, &options,*(s_audio->context_out), &s_audio->stream_out);if (ret < 0){printf("initStream failed\n");return -1;}AVStream *stream_a = s_audio->context_in->streams[s_audio->streamid];printf("audio stream, sample_rate: %d, channels: %d, format: %s\n",stream_a->codecpar->sample_rate, stream_a->codecpar->channels,av_get_sample_fmt_name((enum AVSampleFormat)stream_a->codecpar->format));ret = setAcodec(&s_audio->codec_context, stream_a, *(s_audio->context_out), s_audio->stream_out);if (ret < 0){printf("setAcodec failed\n");return -1;}ret = initSwr(&s_audio->swr_ctx, stream_a, s_audio->stream_out);if (ret < 0){printf("initSwr failed\n");return -1;}return 0;
}void *thread_v(void *arg)
{int ret = -1;int64_t frame_index = 0;st_video *s_video = (st_video *)arg;AVStream *stream_v = s_video->context_in->streams[s_video->streamid];// 分配内存AVFrame *input_frame = av_frame_alloc();AVFrame *frame_yuv420p = av_frame_alloc();if (!input_frame || !frame_yuv420p){printf("av_frame_alloc error\n");goto end;}AVPacket *packet = av_packet_alloc();if (!packet){printf("av_packet_alloc failed\n");goto end;}// 设置帧格式input_frame->format = s_video->camera_pix_fmt;input_frame->width = stream_v->codecpar->width;input_frame->height = stream_v->codecpar->height;frame_yuv420p->format = AV_PIX_FMT_YUV420P;frame_yuv420p->width = stream_v->codecpar->width;frame_yuv420p->height = stream_v->codecpar->height;// 分配帧内存ret = av_frame_get_buffer(frame_yuv420p, 0);if (ret < 0){printf("av_frame_get_buffer error\n");goto end;}// 读取帧并进行转换AVPacket pkt;while (av_read_frame(s_video->context_in, &pkt) >= 0){if (pkt.stream_index == s_video->streamid){// 把读取的帧数据(AVPacket)拷贝到输入帧(AVFrame)中ret = av_image_fill_arrays(input_frame->data, input_frame->linesize, pkt.data, s_video->camera_pix_fmt,stream_v->codecpar->width, stream_v->codecpar->height, 1);if (ret < 0){av_packet_unref(&pkt);printf("av_image_fill_arrays error\n");break;}// 转换为 YUV420Psws_scale(s_video->sws_ctx, (const uint8_t *const *)input_frame->data, input_frame->linesize, 0,input_frame->height, frame_yuv420p->data, frame_yuv420p->linesize);frame_yuv420p->pts = frame_index;frame_index++;// 发送帧到编码器ret = avcodec_send_frame(s_video->codec_context, frame_yuv420p);if (ret < 0){printf("avcodec_send_frame error (errmsg '%s')\n", av_err2str(ret));break;}// 接收编码后的数据包while (ret >= 0){ret = avcodec_receive_packet(s_video->codec_context, packet);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){break;}else if (ret < 0){printf("avcodec_receive_packet error (errmsg '%s')\n", av_err2str(ret));goto end;}packet->stream_index = s_video->stream_out->index;// 将时间戳从编码器时间基转换到流时间基av_packet_rescale_ts(packet, s_video->codec_context->time_base, s_video->stream_out->time_base);packet->pos = -1;// 推送到RTMP服务器pthread_mutex_lock(s_video->lock_write_frame);ret = av_interleaved_write_frame(*(s_video->context_out), packet);pthread_mutex_unlock(s_video->lock_write_frame);if (ret < 0){printf("av_interleaved_write_frame error (errmsg '%d')\n", ret);av_packet_unref(packet);goto end;}av_packet_unref(packet);}}av_packet_unref(&pkt);}end:// 释放资源if (input_frame)av_frame_free(&input_frame);if (frame_yuv420p)av_frame_free(&frame_yuv420p);if (packet)av_packet_free(&packet);return NULL;
}void *thread_a(void *arg)
{st_audio *s_audio = (st_audio *)arg;int ret = -1;int fsize = 0;int64_t pts = 0;AVFrame *frame_out = NULL;AVAudioFifo *fifo = NULL;frame_out = av_frame_alloc();if (!frame_out){printf("av_frame_alloc failed\n");goto end;}// 设置帧参数, av_frame_get_buffer 在分配缓冲区时会用到frame_out->format = s_audio->codec_context->sample_fmt;frame_out->nb_samples = s_audio->codec_context->frame_size;frame_out->channel_layout = s_audio->codec_context->channel_layout;// 分配帧缓冲区ret = av_frame_get_buffer(frame_out, 0);if (ret < 0){printf("av_frame_get_buffer failed\n");goto end;}AVStream *stream_a = s_audio->context_in->streams[s_audio->streamid];// 计算编码每帧aac所需的pcm数据的大小 = 采样个数 * 采样格式大小 * 声道数fsize = s_audio->codec_context->frame_size *av_get_bytes_per_sample(stream_a->codecpar->format) *stream_a->codecpar->channels;printf("frame size: %d\n", fsize);fifo = av_audio_fifo_alloc((enum AVSampleFormat)stream_a->codecpar->format,stream_a->codecpar->channels, s_audio->codec_context->frame_size * 5);if (!fifo){printf("av_audio_fifo_alloc failed\n");goto end;}uint8_t *buf = av_malloc(fsize);if (!buf){printf("av_malloc failed\n");goto end;}AVPacket *recv_ptk = av_packet_alloc();if (!recv_ptk){printf("av_packet_alloc failed\n");goto end;}int sample_size = av_get_bytes_per_sample(stream_a->codecpar->format);// 读取帧AVPacket read_pkt;while (av_read_frame(s_audio->context_in, &read_pkt) >= 0){if (read_pkt.stream_index == s_audio->streamid){av_audio_fifo_write(fifo, (void **)&read_pkt.buf->data,read_pkt.size / sample_size);if (av_audio_fifo_size(fifo) < s_audio->codec_context->frame_size){// 不够一帧aac编码所需的数据continue;}av_audio_fifo_read(fifo, (void **)&buf, s_audio->codec_context->frame_size);// 重采样ret = swr_convert(s_audio->swr_ctx, frame_out->data, frame_out->nb_samples,(const uint8_t **)&buf, frame_out->nb_samples);if (ret < 0){printf("swr_convert failed\n");goto end;}frame_out->pts = pts;pts += frame_out->nb_samples;// 发送帧给编码器ret = avcodec_send_frame(s_audio->codec_context, frame_out);if (ret < 0){printf("avcodec_send_frame failed\n");goto end;}// 接收编码后的数据包while (ret >= 0){ret = avcodec_receive_packet(s_audio->codec_context, recv_ptk);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){break;}else if (ret < 0){printf("avcodec_receive_packet error (errmsg '%s')\n", av_err2str(ret));goto end;}recv_ptk->stream_index = s_audio->stream_out->index;av_packet_rescale_ts(recv_ptk, s_audio->codec_context->time_base,s_audio->stream_out->time_base);pthread_mutex_lock(s_audio->lock_write_frame);ret = av_interleaved_write_frame(*s_audio->context_out, recv_ptk);pthread_mutex_unlock(s_audio->lock_write_frame);if (ret < 0){printf("av_interleaved_write_frame failed\n");av_packet_unref(recv_ptk);goto end;}av_packet_unref(recv_ptk);}}av_packet_unref(&read_pkt);}end:if (frame_out)av_frame_free(&frame_out);if (recv_ptk)av_packet_free(&recv_ptk);if (fifo)av_audio_fifo_free(fifo);if (buf)av_free(buf);return NULL;
}


相关博客链接:FFmpeg 实现从摄像头获取流并通过RTMP推流
                         FFmpeg 实现从麦克风获取流并通过RTMP推流 


http://www.ppmy.cn/news/1516509.html

相关文章

python手写了个简易的豆瓣影评爬虫

使用python手写了个简易的豆瓣影评爬虫代码。 __author__ wsximport time import requests from bs4 import BeautifulSoup import os import re import uuiddef clean_windows_filename(string_file_name):invalid_chars r[\\/:*?"<>|]return re.sub(invalid_c…

ZooKeeper 的特性及其在分布式系统中的配置中心的应用

以下是配置管理和服务注册的实现方式&#xff1a; 1. 配置管理 配置管理指的是将系统中各个组件的配置信息集中管理&#xff0c;以便动态更新和统一配置。ZooKeeper 可以用来管理配置文件&#xff0c;通过它的节点结构和数据一致性功能&#xff0c;确保所有客户端都能获得最新…

PIL convert(‘RGB‘) 用法

PIL 不提供 BGR转RGB的方法。 1. 图像模式转换 如果图像当前的模式不是 RGB&#xff0c;例如它是灰度&#xff08;L&#xff09;、CMYK 或其他模式时&#xff0c;convert(RGB) 会将图像转换为 RGB 格式。 灰度图像 (L)&#xff1a; 如果图像是灰度图像 (L)&#xff0c;conver…

【机器学习工具库-一-传统机器学习sklearn库】

sklearn库 安装安装顺序 sklearn库的六大功能sklearn中的核心调用流程 sklearn库是用于机器学习一个工具包&#xff0c;有了它&#xff0c;可以帮我们用简单的函数实现传统机器学习中的分类、聚类等任务。 安装 sklearn的官网 http://scikit-learn.org/stable/ sklearn库基于N…

【C++ 面试 - 内存管理】每日 3 题(二)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/fYaBd &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏&…

Django后端架构开发:Nginx服务优化实践

Django后端架构开发&#xff1a;Nginx服务优化实践 目录 &#x1f31f; Nginx核心概念&#x1f50d; Nginx服务原理&#x1f504; Nginx负载均衡&#x1f517; Nginx反向代理⚙️ Nginx动静分离 &#x1f31f; Nginx核心概念 Nginx作为一款轻量级且功能强大的HTTP服务器&…

分布式性能测试-通篇讲解 Locust 性能测试

分布式性能测试-小试牛刀 Locust 分布式负载生成概述 Locust 支持分布式负载生成,以模拟更高的并发负载。你可以通过以下方式来配置和使用分布式模式: 1. 基本概念 Master 实例:管理整个负载测试,运行 Locust 的 Web 界面,并协调各个 Worker 的任务。Worker 实例:实际…

思特威-秋招正式批-笔试

1.在全局数据区中分配空间的变量类型有哪些 2.new和malloc的区别 3. class CData{unsigned short m_uilndex, m_uilndexFlag 9; int m_iData[10]; int m_iType;int iGetDataType() {return m_iType;} public: CData(); }CData::CData(), m_iType(5) {string strTxt "…

【算法模板】基础:区间合并

区间合并是一种常见的算法问题&#xff0c;通常在处理范围覆盖、时间调度、区间覆盖等问题时会用到。区间合并的目的是将一些有重叠或相邻的区间合并成一个更大的区间&#xff0c;从而简化问题的复杂性。 算法思想 给定一组区间&#xff0c;可能存在部分区间之间有重叠或相邻关…

你是如何克服编程学习中的挫折感的?——从Bug中找到成长的契机

你是如何克服编程学习中的挫折感的&#xff1f; 从Bug中找到成长的契机 在编程的世界里&#xff0c;Bug 是不可避免的。无论是初学者还是经验丰富的开发者&#xff0c;都不可能完全避免 Bug 的出现。与其视 Bug 为敌人&#xff0c;不如将其看作成长的契机。每一个 Bug 的出现&…

虚幻5|简单的设置角色受到伤害,远程攻击机关设置,制作UI,低血量UI

虚幻5|制作玩家血量&#xff0c;体力&#xff08;还未编辑&#xff0c;只用于引用&#xff09;-CSDN博客 需完成制作玩家血量及体力部分 一.给角色添加死亡动画 1.为了保证角色在播放死亡蒙太奇的时候&#xff0c;不会重新播放&#xff0c;而是保持原来倒地的姿势&#xff0…

《黑神话·悟空》是用什么编程语言开发的?

最近火爆全球的国产 3A 大作《黑神话悟空》&#xff0c;你玩了吗&#xff1f;没玩没关系&#xff0c;有人就是对游戏不感冒&#xff0c;我找了个宣发片&#xff0c;一起感受下3A大作的视觉冲击&#xff0c;而且还是我们从小听到大&#xff0c;那猴子&#x1f412;的故事。 ‌‌…

Scrum 敏捷模型、软件测试

三个角色和五大重要会议 三个角色&#xff1a;产品经理、项目经理、研发团队 五个重要会议&#xff1a;需求发布会议、计划发布会议、每日会议、演示会议 每日会议&#xff1a;昨天做了什么&#xff08; 进度&#xff09;、今天做了什么&#xff08;有目标&#xff09;、遇到…

Objective-C中的MVC架构:构建清晰、可维护的iOS应用

标题&#xff1a;Objective-C中的MVC架构&#xff1a;构建清晰、可维护的iOS应用 在iOS开发中&#xff0c;MVC&#xff08;Model-View-Controller&#xff09;架构模式是一种经典的设计模式&#xff0c;用于分离应用的业务逻辑、用户界面和控制逻辑&#xff0c;以提高代码的可…

Flutter-自适用高度PageView

需求 在 Flutter 中&#xff0c;PageView 是一个非常常用的组件&#xff0c;能够实现多个页面的滑动切换。然而&#xff0c;默认的 PageView 高度是固定的&#xff0c;这在展示不同高度的页面时&#xff0c;可能会导致不必要的空白或内容裁剪问题。为了使 PageView 能够根据每…

云计算环境下的等保测评要点分析

在云计算环境下进行等保测评时&#xff0c;需要关注以下几个关键点&#xff1a; 安全责任共担模型&#xff1a;明确云服务提供商&#xff08;CSP&#xff09;与云服务用户&#xff08;CSU&#xff09;之间的安全责任划分&#xff0c;确保双方在安全防护上的协同作用。 安全控制…

【笛卡尔积】深入理解笛卡尔积及其在SQL中的应用

文章目录 引言笛卡尔积的定义数学背景SQL 中的笛卡尔积 SQL 示例基础示例复杂示例使用 WHERE子句限制结果集 笛卡尔积的实际应用笛卡尔积的性能考虑性能影响 更多相关内容可查看 在一个阳光明媚的周一清晨&#xff0c;听到这个词汇突然觉得有点陌生才有了此文的诞生 引言 在数…

33. 二叉搜索树的后序遍历序列【难】

comments: true difficulty: 中等 edit_url: https://github.com/doocs/leetcode/edit/main/lcof/%E9%9D%A2%E8%AF%95%E9%A2%9833.%20%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97/README.md 面试题 33. 二…

破晓科技与神话:三防平板与《黑神话:悟空》的创新交响

当全球游戏圈因《黑神话&#xff1a;悟空》的震撼预告而沸腾&#xff0c;一款代表中国游戏顶尖制作水平的作品&#xff0c;正以它独特的文化魅力与技术创新&#xff0c;向世界宣告着中国游戏产业的崛起。 点击添加图片描述&#xff08;最多60个字&#xff09;编辑 震撼视觉体验…

nginx正向代理与反向代理功能

Nginx是一款高性能的HTTP和反向代理服务器&#xff0c;同时也是一个IMAP/POP3/SMTP代理服务器。它的正向代理和反向代理功能在实际工作中有广泛的应用。 正向代理 功能 正向代理是位于客户端和原始服务器之间的代理服务器。客户端&#xff08;例如浏览器&#xff09;向代理服…