【RTSP從零實踐】2、使用RTP協議封裝并傳輸H264

😁博客主頁😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客內容🤑:🍭嵌入式開發、Linux、C語言、C++、數據結構、音視頻🍭
🤣本文內容🤣:🍭介紹怎么使用RTP協議封裝并傳輸H264🍭
😎金句分享😎:🍭你不能選擇最好的,但最好的會來選擇你——泰戈爾🍭
?發布時間?: 2025-06-07

本文未經允許,不得轉發!!!

目錄

  • 🎄一、概述
  • 🎄二、實現步驟、實現細節
    • ?2.1、實現H.264文件讀取器
    • ?2.2、實現 H264 的 RTP 數據包封裝
      • 🎈2.2.1、RTP頭(RTP Header)
      • 🎈2.2.2、H264 的 RTP負載(RTP Payload)
    • ?2.3、SDP協議介紹
  • 🎄三、RTP協議封裝并傳輸H264的實現源碼
  • 🎄四、總結


在這里插入圖片描述

在這里插入圖片描述

🎄一、概述

上篇文章 根據RTSP協議實現一個RTSP服務,介紹了怎么寫一個RTSP服務端,但處理的過程中,沒有介紹怎么發送RTP包。這篇文章主要就是介紹怎么將H264視頻數據封裝成RTP包并發送的。

首先,需要對 H264的文件結構 有了解,我們要從一個H264文件讀取視頻幀,再進行RTP封包發送。本文會介紹需要用到的h264知識,需要了解更詳細的可以參考以前文章:H.264視頻編碼及NALU詳解。

然后,需要對 RTP包結構 有個了解,本文介紹的RTP包由2部分組成:RTP頭、RTP負載。需要了解RTP頭數據結構、RTP負載數據結構,下個小節會介紹怎樣用代碼實現。對RTP數據包格式需要了解更多的可以參考這篇文章:RTP協議詳解 。

最后,需要對 SDP協議 有了解,后面代碼運行后,需要使用vlc打開.sdp文件來驗證功能,所以需要知道一個sdp文件的內容是什么意思,這個將會在2.3小節介紹,如果需要了解更多sdp協議知識,可以看這篇文章:SDP(會話描述協議)詳解 及 抓包例子分析。


在這里插入圖片描述

🎄二、實現步驟、實現細節

這個小寫介紹一些基礎知識點,下個小節會提供源碼,可以結合著源碼看幫助理解消化。

?2.1、實現H.264文件讀取器

在這里插入圖片描述

H.264文件保存了h264編碼的視頻幀,每個視頻幀之間以開始碼00 00 0100 00 00 01分隔開。我們可以用下面代碼判斷是否為開始碼。
在這里插入圖片描述

在兩個開始碼之間的就是視頻幀數據。h264視頻幀數據的第一個字節是一個NAL頭,內容如下圖:
在這里插入圖片描述
可以用下面代碼讀取NAL頭:
在這里插入圖片描述
以上H264文件結構需要了解的知識,完整代碼可以到第三節查看。


?2.2、實現 H264 的 RTP 數據包封裝

本文介紹的RTP數據包主要由2部分組成,即 RTP頭、RTP負載。

🎈2.2.1、RTP頭(RTP Header)

在這里插入圖片描述
上圖是RTP頭的結構圖,包含了12個字節的內容,可以用代碼定義成如下結構體:

struct RtpHeader
{/* byte 0 */uint8_t csrcLen:4;uint8_t extension:1;uint8_t padding:1;uint8_t version:2;/* byte 1 */uint8_t payloadType:7;uint8_t marker:1;/* bytes 2,3 */uint16_t seq;/* bytes 4-7 */uint32_t timestamp;/* bytes 8-11 */uint32_t ssrc;
};

RTP頭這里涉及到一個 時間戳怎么計算 的問題,需要注意的是,這個時間戳是一個 時鐘頻率 為單位的,而不是具體的時間(秒、毫秒等)。
一般情況下,H264的時鐘頻率為90000Hz,假設幀率為25,那么每一幀的 時間間隔 就是1/25秒,每一幀的 時鐘增量 就是(90000/25=3600)。那時間戳怎么算呢?舉個例子,如果幀率為25的H264視頻,第一幀的RTP時間戳為0的話,那么第二幀的RTP時間戳就是 3600,第三幀的RTP時間戳就是 7200,依次類推,后一幀的RTP時間戳在前一幀的RTP時間戳的值加上一個時鐘增量


🎈2.2.2、H264 的 RTP負載(RTP Payload)

H264 的 RTP負載需要介紹兩種方式,第一種是 單個NAL單元封包(Single NAL Unit Packet);第二種是 分片單元(Fragmentation Unit) 。如果H264的視頻幀NALU(NAL Unit)總字節數小于 MTU(網絡最大傳輸單元1500字節),就可以使用第一種方式,因為有一些TCP/UDP頭數據,所以一般判斷小于1400字節,就采用 單個NAL單元封包(Single NAL Unit Packet),否則使用分片單元(Fragmentation Unit)的方式封裝RTP包。

單個NAL單元封包 的RTP負載結構如下圖,相當于直接將整個NAL Unit 填入RTP負載即可:
在這里插入圖片描述

分片單元的RTP負載方式也有兩種,本文介紹的是FU-A的方式,RTP負載最開始由三部分組成:第一個字節是FU indicator,第二個字節是FU header,第三個字節開始就是NAL單元去掉NAL頭之后的數據:
在這里插入圖片描述

  • FU indicatorFU indicator的大小是一個字節,格式如下,跟NAL頭的格式一樣,但作為 分片RTP封包 ,并不能直接將H264的NAL頭直接填上去。
    F:一般為0。為0表示此NAL單元不應包含bit錯誤或語法違規;為1表示此NAL單元可能包含bit錯誤或語法違規;
    NRI:直接將H264NAL頭的NRI值填入即可;
    Type:FU-A格式的封包填28,FU-B格式的封包填29。

    +---------------+
    |0|1|2|3|4|5|6|7|
    +-+-+-+-+-+-+-+-+
    |F|NRI|  Type   |
    +---------------+
    
  • FU header FU header的大小也是一個字節,格式如下:
    S:start,NALU拆分多個分包后,第一個發送的分包,此bit位置1,其他分包為0;
    E:end,NALU拆分多個分包后,最后一個發送的分包,此bit位置1,其他分包為0;
    R:保留位,必須等于0;
    Type:將H264的NAL頭的負載類型Type直接填入。

    +---------------+
    |0|1|2|3|4|5|6|7|
    +-+-+-+-+-+-+-+-+
    |S|E|R|  Type   |
    +---------------+
    

這部分可以結合下個小節代碼,多看幾篇去理解。


?2.3、SDP協議介紹

本文是使用VLC打開.sdp文件來驗證功能的,所以也需要知道SDP協議的一些知識,下面是本文使用的sdp文件:

m=video 9832 RTP/AVP 96 
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 192.168.2.180
  • m=video 9832 RTP/AVP 96 :表示這是一個媒體描述,媒體類型是video;接收媒體流的端口號是9832;傳輸協議是RTP/AVP(通過UDP傳輸);RTP負載類型為96。

  • a=rtpmap:96 H264/90000:RTP負載類型的具體編碼是 H264,時鐘頻率為 90000Hz。

  • a=framerate:25:幀率為25。

  • c=IN IP4 192.168.2.180:表示連接信息,網絡類型為IN,表示Internet。地址類型為IP4,表示IPv4地址。接收端地址為192.168.2.180。


在這里插入圖片描述

🎄三、RTP協議封裝并傳輸H264的實現源碼

總共有5個源代碼文件,1個sdp文件,具體如下:

1、H264Reader.h

#ifndef __H264_READER_H__
#define __H264_READER_H__#include <stdio.h>#define MAX_STARTCODE_LEN (4)typedef enum
{FALSE,TRUE,
} BOOL;typedef enum
{H264_NALU_TYPE_SLICE = 1,H264_NALU_TYPE_DPA = 2,H264_NALU_TYPE_DPB = 3,H264_NALU_TYPE_DPC = 4,H264_NALU_TYPE_IDR = 5,H264_NALU_TYPE_SEI = 6,H264_NALU_TYPE_SPS = 7,H264_NALU_TYPE_PPS = 8,H264_NALU_TYPE_AUD = 9,H264_NALU_TYPE_EOSEQ = 10,H264_NALU_TYPE_EOSTREAM = 11,H264_NALU_TYPE_FILL = 12,
} H264NaluType;typedef enum
{H264_NALU_PRIORITY_DISPOSABLE = 0,H264_NALU_PRIRITY_LOW = 1,H264_NALU_PRIORITY_HIGH = 2,H264_NALU_PRIORITY_HIGHEST = 3
} H264NaluPriority;typedef struct
{int startcode_len;		  //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)int forbidden_bit;		  //! should be always FALSEint nal_reference_idc;	  //! H264_NALU_PRIORITY_xxxxint nal_unit_type;		  //! H264_NALU_TYPE_xxxxBOOL isLastFrame;		  //!int frame_len;			  //!unsigned char *pFrameBuf; //!
} H264Frame_t;typedef struct H264ReaderInfo_s
{FILE *pFileFd;int frameNum;
} H264ReaderInfo_t;int H264_FileOpen(char *fileName, H264ReaderInfo_t *pH264Info);
int H264_FileClose(H264ReaderInfo_t *pH264Info);
int H264_GetFrame(H264Frame_t *pH264Frame, H264ReaderInfo_t *pH264Info);
BOOL H264_IsEndOfFile(const H264ReaderInfo_t *pH264Info);
void H264_SeekFile(H264ReaderInfo_t *pH264Info);#endif // __H264_READER_H__

2、H264Reader.c

#include "H264Reader.h"
#include <stdlib.h>#define MAX_FRAME_LEN (1920 * 1080 * 1.5) // 一幀數據最大字節數static BOOL findStartCode_001(unsigned char *Buf)
{// printf("[%d %d %d]\n", Buf[0], Buf[1], Buf[2]);return (Buf[0] == 0 && Buf[1] == 0 && Buf[2] == 1); // 0x000001 ?
}static BOOL findStartCode_0001(unsigned char *Buf)
{// printf("[%d %d %d %d]\n", Buf[0], Buf[1], Buf[2], Buf[3]);return (Buf[0] == 0 && Buf[1] == 0 && Buf[2] == 0 && Buf[3] == 1); // 0x00000001 ?
}int H264_FileOpen(char *fileName, H264ReaderInfo_t *pH264Info)
{pH264Info->pFileFd = fopen(fileName, "rb+");if (pH264Info->pFileFd == NULL){printf("[%s %d]Open file error\n", __FILE__, __LINE__);return -1;}pH264Info->frameNum = 0;return 0;
}int H264_FileClose(H264ReaderInfo_t *pH264Info)
{if (pH264Info->pFileFd != NULL){fclose(pH264Info->pFileFd);pH264Info->pFileFd = NULL;}return 0;
}BOOL H264_IsEndOfFile(const H264ReaderInfo_t *pH264Info)
{return feof(pH264Info->pFileFd);
}void H264_SeekFile(H264ReaderInfo_t *pH264Info)
{fseek(pH264Info->pFileFd, 0, SEEK_SET);pH264Info->frameNum = 0;
}/*** @brief 獲取一陣h264視頻幀** @param pH264Frame :輸出參數,使用后 pH264Frame->pFrameBuf 需要free* @param pH264Info :輸入參數* @return int*/
int H264_GetFrame(H264Frame_t *pH264Frame, H264ReaderInfo_t *pH264Info)
{int rewind = 0;if (pH264Info->pFileFd == NULL){printf("[%s %d]pFileFd error\n", __FILE__, __LINE__);return -1;}// 1.讀取幀數據unsigned char *pFrame = (unsigned char *)malloc(MAX_FRAME_LEN);int readLen = fread(pFrame, 1, MAX_FRAME_LEN, pH264Info->pFileFd);if (readLen <= 0){printf("[%s %d]fread error\n", __FILE__, __LINE__);free(pFrame);return -1;}// 2.查找當前幀開始碼int i = 0;for (; i < readLen - MAX_STARTCODE_LEN; i++){if (!findStartCode_0001(&pFrame[i])){if (!findStartCode_001(&pFrame[i])){continue;}else{pH264Frame->startcode_len = 3;break;}}else{pH264Frame->startcode_len = 4;break;}}if (i != 0) // 不是幀開頭,偏移到幀開頭重新讀{printf("[%s %d]startcode error, i=%d\n", __FILE__, __LINE__, i);free(pFrame);rewind = (-(readLen - i));fseek(pH264Info->pFileFd, rewind, SEEK_CUR);return -1;}// 3.查找下一幀開始碼i += MAX_STARTCODE_LEN;for (; i < readLen - MAX_STARTCODE_LEN; i++){if (!findStartCode_0001(&pFrame[i])){if (!findStartCode_001(&pFrame[i])){continue;}else{break;}}else{break;}}if (i == (readLen - MAX_STARTCODE_LEN)){if (!feof(pH264Info->pFileFd)){printf("[%s %d]MAX_FRAME_LEN too small\n", __FILE__, __LINE__);free(pFrame);return -1;}else{pH264Frame->isLastFrame = TRUE;}}// 4.填數據pH264Frame->forbidden_bit = pFrame[pH264Frame->startcode_len] & 0x80;     // 1 bitpH264Frame->nal_reference_idc = pFrame[pH264Frame->startcode_len] & 0x60; // 2 bitpH264Frame->nal_unit_type = pFrame[pH264Frame->startcode_len] & 0x1f;     // 5 bit, naluType 是開始碼后一個字節的最后 5 位pH264Frame->pFrameBuf = pFrame;pH264Frame->frame_len = i;// 5.文件讀取指針偏移到下一幀位置rewind = (-(readLen - i));fseek(pH264Info->pFileFd, rewind, SEEK_CUR);pH264Info->frameNum++;return pH264Frame->frame_len;
}

3、rtp.h

 #ifndef _RTP_H_
#define _RTP_H_
#include <stdint.h>#define RTP_VESION              2#define RTP_PAYLOAD_TYPE_H264   96
#define RTP_PAYLOAD_TYPE_AAC    97#define RTP_HEADER_SIZE         12
#define RTP_MAX_PKT_SIZE        1400/***    0                   1                   2                   3*    7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |V=2|P|X|  CC   |M|     PT      |       sequence number         |*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |                           timestamp                           |*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*   |           synchronization source (SSRC) identifier            |*   +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+*   |            contributing source (CSRC) identifiers             |*   :                             ....                              :*   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+**/
struct RtpHeader
{/* byte 0 */uint8_t csrcLen:4;uint8_t extension:1;uint8_t padding:1;uint8_t version:2;/* byte 1 */uint8_t payloadType:7;uint8_t marker:1;/* bytes 2,3 */uint16_t seq;/* bytes 4-7 */uint32_t timestamp;/* bytes 8-11 */uint32_t ssrc;
};struct RtpPacket
{struct RtpHeader rtpHeader;uint8_t payload[0];
};void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,uint16_t seq, uint32_t timestamp, uint32_t ssrc);
int rtpSendPacket(int socket, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize);#endif //_RTP_H_

4、rtp.c

 
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "rtp.h"void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,uint16_t seq, uint32_t timestamp, uint32_t ssrc)
{rtpPacket->rtpHeader.csrcLen = csrcLen;rtpPacket->rtpHeader.extension = extension;rtpPacket->rtpHeader.padding = padding;rtpPacket->rtpHeader.version = version;rtpPacket->rtpHeader.payloadType =  payloadType;rtpPacket->rtpHeader.marker = marker;rtpPacket->rtpHeader.seq = seq;rtpPacket->rtpHeader.timestamp = timestamp;rtpPacket->rtpHeader.ssrc = ssrc;
}int rtpSendPacket(int socket, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize)
{struct sockaddr_in addr;int ret;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);ret = sendto(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE, 0,(struct sockaddr*)&addr, sizeof(addr));rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);return ret;
}

5、rtp_h264_main.c


#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>#include "rtp.h"
#include "H264Reader.h"#define H264_FILE_NAME "test.h264"
#define CLIENT_IP "192.168.2.180" // 運行vlc打開sdp文件的電腦IP
#define CLIENT_PORT 9832
#define FPS 25static int createUdpSocket()
{int fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0)return -1;int on = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on));return fd;
}static int rtpSendH264Frame(int socket, char *ip, int16_t port,struct RtpPacket *rtpPacket, uint8_t *frame, uint32_t frameSize)
{uint8_t naluType; // nalu第一個字節int sendBytes = 0;int ret;naluType = frame[0];if (frameSize <= RTP_MAX_PKT_SIZE) // nalu長度小于最大包場:單一NALU單元模式{/**   0 1 2 3 4 5 6 7 8 9*  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*  |F|NRI|  Type   | a single NAL unit ... |*  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*/memcpy(rtpPacket->payload, frame, frameSize);ret = rtpSendPacket(socket, ip, port, rtpPacket, frameSize);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加時間戳goto out;}else // nalu長度大于最大包場:分片模式{/**  0                   1                   2*  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+* | FU indicator  |   FU header   |   FU payload   ...  |* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+*//**     FU Indicator*    0 1 2 3 4 5 6 7*   +-+-+-+-+-+-+-+-+*   |F|NRI|  Type   |*   +---------------+*//**      FU Header*    0 1 2 3 4 5 6 7*   +-+-+-+-+-+-+-+-+*   |S|E|R|  Type   |*   +---------------+*/int pktNum = frameSize / RTP_MAX_PKT_SIZE;        // 有幾個完整的包int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小int i, pos = 1;/* 發送完整的包 */for (i = 0; i < pktNum; i++){rtpPacket->payload[0] = (naluType & 0x60) | 28;rtpPacket->payload[1] = naluType & 0x1F;if (i == 0)                                     // 第一包數據rtpPacket->payload[1] |= 0x80;              // startelse if (remainPktSize == 0 && i == pktNum - 1) // 最后一包數據rtpPacket->payload[1] |= 0x40;              // endmemcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE);ret = rtpSendPacket(socket, ip, port, rtpPacket, RTP_MAX_PKT_SIZE + 2);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;pos += RTP_MAX_PKT_SIZE;}/* 發送剩余的數據 */if (remainPktSize > 0){rtpPacket->payload[0] = (naluType & 0x60) | 28;rtpPacket->payload[1] = naluType & 0x1F;rtpPacket->payload[1] |= 0x40; // endmemcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2);ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize + 2);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;}}out:return sendBytes;
}int main(int argc, char *argv[])
{int socket = createUdpSocket();if (socket < 0){printf("failed to create socket\n");return -1;}struct RtpPacket *rtpPacket = (struct RtpPacket *)malloc(sizeof(struct RtpPacket) + (1920 * 1080 * 4));rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0,0, 0, 0x88923423);// h264H264ReaderInfo_t h264Info;if (H264_FileOpen(H264_FILE_NAME, &h264Info) < 0){printf("failed to open %s\n", H264_FILE_NAME);return -1;}while (1){if (!H264_IsEndOfFile(&h264Info)){H264Frame_t h264Frame;memset(&h264Frame, 0, sizeof(h264Frame));H264_GetFrame(&h264Frame, &h264Info);if (h264Frame.pFrameBuf != NULL){if (h264Frame.isLastFrame) // 最后一幀,移到開頭重新讀{printf("warning SeekFile 1\n");H264_SeekFile(&h264Info);}// printf("rtpSendH264Frame\n");rtpSendH264Frame(socket, CLIENT_IP, CLIENT_PORT, rtpPacket,h264Frame.pFrameBuf + h264Frame.startcode_len,h264Frame.frame_len - h264Frame.startcode_len);free(h264Frame.pFrameBuf);rtpPacket->rtpHeader.timestamp += 90000 / FPS; // RTP 傳輸視頻每秒 90k HZusleep(1000 * 1000 / FPS);}}}free(rtpPacket);return 0;
}

6、rtp_h264.sdp

m=video 9832 RTP/AVP 96 
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 192.168.2.180

將上面代碼保存在同一個目錄后,并且在同目錄里放一個.h264文件,然后運行 gcc *.c 編譯,再執行./a.out運行程序,下面是我運行的過程:
在這里插入圖片描述


在這里插入圖片描述

🎄四、總結

本文介紹了實現 使用RTP協議封裝并傳輸H264 的一些步驟和細節,介紹了RTP封包格式,sdp相關知識等,也提供了實現源碼和運行結果,可以幫助讀者快速了解RT協議。

在這里插入圖片描述
如果文章有幫助的話,點贊👍、收藏?,支持一波,謝謝 😁😁😁

參考:
https://blog.csdn.net/huabiaochen/article/details/104550107

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/87660.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/87660.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/87660.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

行業熱點丨手機中框設計如何體現增材思維?

編者薦語&#xff1a; 通過增材設計思維在金屬邊框設計晶格結構&#xff0c;既能減輕重量&#xff0c;同時也有助于散熱&#xff0c;針對不同位置設計不同類型的晶格結構還能起到緩沖效果&#xff0c;提高手機抗沖擊能力。 以下文章來源于Inspire增材創新設計&#xff0c;作者…

鴻蒙案例實戰——添加水印

本示例為開發者展示常用的水印添加能力&#xff0c;包括兩種方式給頁面添加水印、保存圖片添加水印、拍照圖片添加水印和pdf文件添加水印。 案例效果截圖 首頁 頁面水印 圖片水印 pdf水印 案例運用到的知識點 核心知識點 頁面添加水印&#xff1a;封裝Canv…

Qt工作總結07 <qBound和std::clamp>

一、qBound簡介 1. 定義 是 Qt 框架中一個非常實用的邊界限制函數&#xff08;也稱為 "clamp" 函數&#xff09;&#xff0c;用于將一個值限制在指定的最小值和最大值之間。頭文件&#xff1a;#include <QtGlobal> 2. 函數原型 template <typename T>…

53-Oracle sqlhc多版本實操含(23 ai)

SQLHC&#xff08;SQL Health Check&#xff09;作為 Oracle 數據庫性能診斷的核心工具&#xff0c;其設計理念和核心功能在 Oracle 各版本中保持高度一致&#xff0c;但在技術實現和周邊生態上存在漸進式優化。定期對關鍵業務 SQL 執行健康檢查&#xff0c;特別是在版本升級或…

math.pow()和pow()的區別

math.pow() 和 pow() 的區別 ? 1. math.pow() 來自 math 模塊參數&#xff1a;兩個數&#xff08;底數&#xff0c;指數&#xff09;結果類型&#xff1a; 始終返回 float 類型 示例&#xff1a; import math print(math.pow(2, 3)) # 輸出&#xff1a;8.0 &#xff08;…

郵科OEM攝像頭POE供電技術的核心優勢

安防監控中&#xff0c;供電方式影響系統穩定性、安裝效率與維護成本。郵科攝像頭采用POE技術&#xff0c;通過網線同時傳輸數據與電力&#xff0c;革新傳統部署模式。本文解析其六大核心優勢&#xff0c;展現其作為現代安防優選方案的價值。 一、布線簡化&#xff0c;效率提升…

微信小程序-數據加密

npm install crypto-js utils/aes.js const CryptoJS require(crypto-js);// 默認的 KEY 與 iv 如果沒有給 const KEY CryptoJS.enc.Utf8.parse(KrQ4KAYOEyAz66RS); // 十六位十六進制數作為密鑰 const IV CryptoJS.enc.Utf8.parse(ep1YCmxXuxKe4eH1); // 十六位十六進制…

【時時三省】(C語言基礎)善于利用指針

山不在高&#xff0c;有仙則名。水不在深&#xff0c;有龍則靈。 ----CSDN 時時三省 指針是C語言中的一個重要概念&#xff0c;也是C語言的一個重要特色。正確而靈活地運用它&#xff0c;可以使程序簡潔、緊湊、高效。每一個學習和使用C語言的人&#xff0c;都應當深入地學習和…

單點登錄進階:基于芋道(yudao)授權碼模式的單點登錄流程、代碼實現與安全設計

最近遇到需要單點登錄的場景&#xff0c;我使用的是芋道框架&#xff0c;正好它手動實現了OAuth2的功能&#xff0c;可以為單點登錄提供一些幫助&#xff0c;結合授權碼的模式&#xff0c;在改動最小的情況下實現了單點登錄。關鍵業務數據已經隱藏&#xff0c;后續將以以主認證…

關于Seata的一個小issue...

文章目錄 引言原因&#x1f913;解決方法&#x1f635;總結?? 引言 某一天&#xff0c;筆者在逛著Github的時候&#xff0c;突然看到seata有個有趣的issue&#xff0c;是一個task。 相關描述&#xff1a; While running the DruidSQLRecognizerFactoryTest.testIsSqlSynta…

FTTR+軟路由網絡拓撲方案

文章目錄 網絡拓撲軟路由配置FTTR光貓路由器TPLink路由器配置WAN設置LAN設置 參考 網絡拓撲 軟路由配置 配置靜態IP地址&#xff1a;192.168.1.100設置網關指向主路由的IP 設置自定義DNS服務器 開啟DHCP 這一步很關鍵&#xff0c;可以讓連上wifi的所有設備自動趴強。 FTTR光貓…

RPC - 服務注冊與發現模塊

為什么要服務注冊&#xff0c;服務注冊是做什么 服務注冊主要是實現分布式的系統&#xff0c;讓系統更加的健壯&#xff0c;一個節點主機將自己所能提供的服務&#xff0c;在注冊中心進行登記 為什么要服務發現&#xff0c;服務發現是要做什么 rpc調用者需要知道哪個節點主機…

分布式緩存:應對突發流量的緩存體系構建

文章目錄 緩存全景圖Pre背景與目標說明緩存原則與設計思路緩存體系架構緩存預熱與緩存預加載庫存操作與緩存結合防刷、限流與緩存緩存一致性與失效異步落地與消息隊列監控與指標容災與擴展示例小結 緩存全景圖 Pre 分布式緩存&#xff1a;緩存設計三大核心思想 分布式緩存&am…

華為云Flexus+DeepSeek征文|CCE容器高可用部署搭建Dify-LLM平臺部署AI Agent

華為云FlexusDeepSeek征文&#xff5c;CCE容器高可用部署搭建Dify-LLM平臺部署AI Agent 前言 Dify是一款開源的大語言模型應用開發平臺&#xff0c;融合了后端即服務和LLMOps的理念&#xff0c;使開發者可以快速搭建生產級的生成式AI應用&#xff0c;本文將詳細介紹如何使用華…

Postman 的 Jenkins 管理 - 手動構建

目錄 一、準備工作 二、postman 項目腳本準備并導出 1. 打開已完成并測試無誤的 postman 項目腳本。 再次執行測試。 ?編輯2. 導出&#xff08; 測試用例集、環境變量 兩個文件&#xff09;**“不 支 持 中 文”** —— 全部改成英文&#xff01; ?編輯3. 文件所在目錄…

音視頻之H.264/AVC解碼器的原理和實現

系列文章&#xff1a; 1、音視頻之視頻壓縮技術及數字視頻綜述 2、音視頻之視頻壓縮編碼的基本原理 3、音視頻之H.264/AVC編碼器原理 4、音視頻之H.264的句法和語義 5、音視頻之H.264/AVC解碼器的原理和實現 6、音視頻之H.264視頻編碼傳輸及其在移動通信中的應用 7、音視…

【智能安全帽新升級】搭載VTX316TTS語音合成芯片,讓安全“聽得見”!

在工地轟鳴的機械聲中&#xff0c;一句清晰的指令可能比任何文字都更有力量。 當智能安全帽遇上VTX316語音合成芯片&#xff0c;安全防護從“被動響應”進化為“主動交互”&#xff0c;為高危行業戴上了一頂“會說話的智慧大腦”&#xff01; 傳統安全帽的“沉默”危機 在建筑…

【目標檢測】非極大值抑制(NMS)的原理與實現

&#x1f9d1; 博主簡介&#xff1a;曾任某智慧城市類企業算法總監&#xff0c;目前在美國市場的物流公司從事高級算法工程師一職&#xff0c;深耕人工智能領域&#xff0c;精通python數據挖掘、可視化、機器學習等&#xff0c;發表過AI相關的專利并多次在AI類比賽中獲獎。CSDN…

DB-GPT啟動提示please install by running `pip install cryptography`

DB-GPT項目需要 cryptography 庫來處理加密功能&#xff0c;但環境中沒有安裝它。cryptography 是一個用于安全和加密操作的Python庫&#xff0c;許多項目&#xff08;包括DB-GPT&#xff09;依賴它來處理敏感數據的加密存儲。 解決方案 1. 安裝 cryptography 庫 在激活的環…

局域網文件共享及檢索系統

標題:局域網文件共享及檢索系統 內容:1.摘要 隨著信息技術的飛速發展&#xff0c;局域網在企業、學校等場景中得到廣泛應用&#xff0c;大量文件在局域網內存儲和流轉。然而&#xff0c;目前局域網內文件共享與檢索存在效率低、管理困難等問題。本文旨在設計并實現一個高效的局…