FFmpeg for Android 图传Web

news/2025/11/12 20:16:37/文章来源:https://www.cnblogs.com/dddy/p/19215272

FFmpeg for Android & 图传Web

之前在项目研发中有个需求, 需要对接RTSP摄像头, 并且需要将其转封装格式到H5能够播放的格式, 并且需要在纯安卓APP中实现, 并且需要低延迟.

找了一圈都没有合适的且现成的方案, 这个需求有那么小众么? so.. 决定自己动手丰衣足食, 然而整个比想象中复杂一点点..

Android 交叉编译 FFmpeg

FFmpeg 官文 - https://www.ffmpeg.org/documentation.html

请参考之前的笔记博客: FFmpeg 编译 (For Android)

NDK 引用实例

CMakeLists.txt

project("ffmpegInterface")# 使用相对路径
set(FFMPEG_DIR_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg/include)# set(FFMPEG_DIR_LIB ${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg/lib)
# 若想自动打包到APK , 添加到 jniLibs 目录下
set(FFMPEG_DIR_LIB E:/projects-my/ffmpeg-make/app/src/main/jniLibs)
# 根据 ABI 设置具体路径
if(${ANDROID_ABI} STREQUAL armeabi-v7a)set(FFMPEG_INCLUDE_DIR ${FFMPEG_DIR_INCLUDE}/armeabi-v7a)set(FFMPEG_LIB_DIR ${FFMPEG_DIR_LIB}/armeabi-v7a)
elseif(${ANDROID_ABI} STREQUAL arm64-v8a)set(FFMPEG_INCLUDE_DIR ${FFMPEG_DIR_INCLUDE}/arm64-v8a)set(FFMPEG_LIB_DIR ${FFMPEG_DIR_LIB}/arm64-v8a)
elseif(${ANDROID_ABI} STREQUAL x86)set(FFMPEG_INCLUDE_DIR ${FFMPEG_DIR_INCLUDE}/x86)set(FFMPEG_LIB_DIR ${FFMPEG_DIR_LIB}/x86)
elseif(${ANDROID_ABI} STREQUAL x86_64)set(FFMPEG_INCLUDE_DIR ${FFMPEG_DIR_INCLUDE}/x86_64)set(FFMPEG_LIB_DIR ${FFMPEG_DIR_LIB}/x86_64)
else()message(FATAL_ERROR "Unsupported ABI: ${ANDROID_ABI}")
endif()# 添加头文件搜索路径
include_directories(${FFMPEG_INCLUDE_DIR})# 添加库文件搜索路径
link_directories(${FFMPEG_LIB_DIR})# 生成
add_library(${CMAKE_PROJECT_NAME} SHARED# List C/C++ source files with relative paths to this CMakeLists.txt.ffmpegInterface.cpp)target_link_libraries(${CMAKE_PROJECT_NAME}# List libraries link to the target libraryavcodec avfilter avformat avutil swresample swscaleandroidlog)
CMake 链接不等于打包

即使在 target_link_libraries 中链接了相关库,这只是编译时链接, 运行时仍需要实际的 .so 文件存在于设备中..

自动打包机制:Android Gradle Plugin 会自动将 src/main/jniLibs/ 目录下的原生库文件打包到 APK 中
目录结构需要按照 ABI 架构组织子目录:

app/src/main/jniLibs/
├─arm64-v8a
│      libavcodec.so
│      libavfilter.so
│      libavformat.so
│      libavutil.so
│      libswresample.so
│      libswscale.so
│      
└─x86_64libavcodec.solibavdevice.solibavfilter.solibavformat.solibavutil.solibswresample.solibswscale.so

FFmpegInterface.kt

package org.yang.webrtc.ffmpeg.make.ffmpegclass FFmpegInterface {companion object {init {System.loadLibrary("ffmpegInterface")}}external fun ffmpegInit() :String
}

ffmpegInterface.cpp

#include <jni.h>
#include <string>extern "C"
{#include <libavcodec/avcodec.h>#include <libavformat/avformat.h>#include <libavfilter/avfilter.h>#include <libavutil/avutil.h>#include <libswscale/swscale.h>#include <libavutil/log.h>
}//打印 FFMPEG 的支持情况
extern "C"
JNIEXPORT void JNICALL
Java_org_yang_webrtc_ffmpeg_make_ffmpeg_FFmpegInterface_ffmpegInit(JNIEnv *env, jobject thiz) {avformat_network_init();LOGI("FFmpeg version is [%s]  ", av_version_info());// 打印FFmpeg版本信息LOGI("===== FFmpeg Version Information =====");LOGI("av_version_info(): %s", av_version_info());LOGI("avcodec_version(): %d.%d.%d", AV_VERSION_MAJOR(avcodec_version()),AV_VERSION_MINOR(avcodec_version()),AV_VERSION_MICRO(avcodec_version()));LOGI("avformat_version(): %d.%d.%d", AV_VERSION_MAJOR(avformat_version()),AV_VERSION_MINOR(avformat_version()),AV_VERSION_MICRO(avformat_version()));LOGI("avutil_version(): %d.%d.%d", AV_VERSION_MAJOR(avutil_version()),AV_VERSION_MINOR(avutil_version()),AV_VERSION_MICRO(avutil_version()));// 打印支持的协议LOGI("\n===== Supported Protocols =====");void* opaque = NULL;const char* name;LOGI("Input Protocols:");while ((name = avio_enum_protocols(&opaque, 0))) {LOGI("  - %s", name);}opaque = NULL;LOGI("\nOutput Protocols:");while ((name = avio_enum_protocols(&opaque, 1))) {LOGI("  - %s", name);}// 打印支持的编码器LOGI("\n===== Supported Encoders =====");const AVCodec *codec = NULL;void *iter = NULL;while ((codec = av_codec_iterate(&iter))) {if (av_codec_is_encoder(codec)) {LOGI("Encoder: %-20s [%s]", codec->name, codec->long_name);}}// 打印支持的解码器LOGI("\n===== Supported Decoders =====");iter = NULL;while ((codec = av_codec_iterate(&iter))) {if (av_codec_is_decoder(codec)) {LOGI("Decoder: %-20s [%s]", codec->name, codec->long_name);}}// 打印支持的封装格式LOGI("\n===== Supported Muxers (Output Formats) =====");const AVOutputFormat *ofmt = NULL;iter = NULL;while ((ofmt = av_muxer_iterate(&iter))) {LOGI("Muxer: %-15s [%s]", ofmt->name, ofmt->long_name);}// 打印支持的解封装格式LOGI("\n===== Supported Demuxers (Input Formats) =====");const AVInputFormat *ifmt = NULL;iter = NULL;while ((ifmt = av_demuxer_iterate(&iter))) {LOGI("Demuxer: %-15s [%s]", ifmt->name, ifmt->long_name);}// 打印支持的硬件加速方法LOGI("\n===== Supported Hardware Acceleration Methods =====");enum AVHWDeviceType type = AV_HWDEVICE_TYPE_NONE;while ((type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE) {LOGI("  - %s", av_hwdevice_get_type_name(type));}// 打印支持的滤镜LOGI("\n===== Supported Filters =====");const AVFilter *filter = NULL;iter = NULL;while ((filter = av_filter_iterate(&iter))) {LOGI("Filter: %-20s [%s]", filter->name, filter->description);}// 打印配置信息LOGI("\n===== Build Configuration =====");LOGI("%s", avcodec_configuration());LOGI("\nFFmpeg initialization complete");
}
/*
extern "C"
JNIEXPORT jstring JNICALL
Java_org_yang_webrtc_ffmpeg_make_ffmpeg_FFmpegInterface_ffmpegInit(JNIEnv *env, jobject thiz) {std::string path = "ffmpeg version is :" + std::string(av_version_info()) ;return env->NewStringUTF(path.c_str()  );//返回字符串
}
*/

关于WebRTC

WebRTC​​ 的全称是 ​​Web Real-Time Communication​​,即网页实时通信。它是一套开放的、允许网页浏览器直接进行实时音视频通话和数据共享的 ​​API 和协议集合​​。

​核心价值​​:在 WebRTC 出现之前,要想在浏览器里进行视频聊天,必须安装像 Flash、Silverlight 这样的插件。WebRTC 将其标准化,使得开发者仅需使用 JavaScript API 就能实现高质量的实时通信功能,无需任何插件。

关键概念

媒体流 - MediaStream

一个 MediaStream对象代表一个同步的媒体数据流。比如,它可以是:

  • ​视频轨​
  • ​音频轨
    你可以把一个 MediaStream理解成一个​
    ​容器​**​,里面装了一条或多条“轨道”。

信令 - Signaling

WebRTC 本身是​​点对点​​ 的,但​​建立P2P连接所需要的信息交换,WebRTC标准并未规定​​。这个交换信息的过程就是​​信令​​。

​信令信道的作用​​:就像两个陌生人要打电话,他们需要先通过某种方式(比如短信)告诉对方自己的电话号码。这个“发短信”的通道就是信令信道。

​信令交换的信息包括​​:

  • ​会话描述协议​​:也就是“我想和你通话,这是我的媒体能力(支持哪些编解码器、分辨率等)”。
  • ​网络信息​​:也就是“这是我的网络地址(IP和端口),你可以通过这个地址找到我”。

in short 规范没有定义信令的数据传输方式​,通常是使用 ​​WebSocket​​ 或 ​​HTTP​​ 长轮询等技术,通过你自己的服务器在两个浏览器之间传递这些信息。

对点连接 - RTCPeerConnection

负责建立和管理两个浏览器之间的​​安全、高效、稳定的点对点连接​​。它处理了所有最复杂的事情:

  • ​NAT 穿透​​:大多数设备都在路由器后面,没有公网 IP。RTCPeerConnection使用 ​​ICE 框架​​(结合 STUN/TURN 服务器)来尝试建立直接的 P2P 连接。
  • ​编解码处理​​:自动协商双方都支持的音视频编解码器(如 VP8, VP9, H.264)。
  • ​网络适应性​​:根据网络状况(带宽、延迟、丢包)动态调整视频质量、码率等。
  • ​加密传输​​:所有传输的数据都是强制加密的。

数据通道 - RTCDataChannel

除了音视频,RTCPeerConnection还允许你建立一个点对点的​​数据通道​​,可以用于传输任意数据,比如文字聊天、文件传输、游戏状态同步等。它类似于 WebSocket,但是是 P2P 的,延迟更低。

关键API

API 方法 作用说明 格式(参数)
RTCPeerConnection() 创建一个新的 WebRTC 连接对象。 new RTCPeerConnection([configuration])
createOffer() 由呼叫方创建一份“提议”,包含本端的媒体和能力信息。 pc.createOffer().then(offer => { ... })
setLocalDescription() 将创建的“提议”或“应答(answer)”设置为本地的描述。 pc.setLocalDescription(offer)
createAnswer() 由接收方根据对方的“提议”创建一份“应答(answer)”。 pc.createAnswer().then(answer => { ... })
setRemoteDescription() 将对方发送过来的“提议”或“应答(answer)”设置为远端的描述。 pc.setRemoteDescription(answer)
addTrack() 将本地音视频轨道添加到连接中。 pc.addTrack(track, stream)
createDataChannel() 创建一个用于传输任意数据的通道。 pc.createDataChannel('channelName')

一个典型的 1对1 视频通话工作流程

假设用户 A 想和用户 B 进行视频通话。

  1. ​媒体捕获​​:

    • A 和 B 的浏览器都调用 getUserMedia获取本地的摄像头和麦克风媒体流。
  2. ​创建连接对象​​:

    • A 和 B 各自创建一个 RTCPeerConnection对象。
  3. ​**​信令交换 -

    信令交换流程​ 纯浏览器:

    1. ​呼叫方​​ 调用 createOffer()生成一个包含其 SDP 的“提议”。
    2. 通过 ​​信令服务器​​(如 WebSocket)将这个 SDP 提议发送给​​接收方​​。
    3. ​接收方​​ 收到 SDP 提议后,调用 setRemoteDescription(offer)告诉浏览器对方的信息。
    4. ​接收方​​ 调用 createAnswer()生成一个对应的 SDP“应答(answer)”。
    5. 通过 ​​信令服务器​​ 将这个 SDP 应答(answer)发回给​​呼叫方​​。
    6. ​呼叫方​​ 收到 SDP 应答(answer)后,调用 setRemoteDescription(answer)
       
    // 创建要约
    const peerConnection = new RTCPeerConnection();
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);// 查看要约 SDP 内容
    console.log('要约 SDP:', offer.sdp);// 发送要约到对方(通过信令服务器)
    signalingServer.send({type: 'offer',sdp: offer.sdp
    });// 对方收到要约后
    await peerConnection.setRemoteDescription(offer);
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);// 查看应答 SDP 内容
    console.log('应答 SDP:', answer.sdp);
    
  4. ​ICE 候选交换​​:

    • 在创建 RTCPeerConnection和设置描述的过程中,A 和 B 会发现自己的网络地址(称为 ​​ICE 候选​​)。
    • 每当发现一个新的候选地址,他们就会通过​​信令服务器​​将这个候选地址发送给对方。
    • 对方通过 pc.addIceCandidate(candidate)来添加这个网络路径。
  5. ​建立 P2P 连接 & 传输流​​:

    • 当信令和 ICE 候选交换完成后,RTCPeerConnection会尝试所有可能的网络路径来建立直接连接。
    • 连接建立成功后,A 和 B 就可以开始传输音视频数据了。
    • A 通过 pc.addTrack(stream.getVideoTracks()[0], stream)将他的视频轨道添加到连接中。
    • B 的浏览器会触发 ontrack事件,在事件回调中,B 可以将接收到的 A 的视频流绑定到一个 <video>元素上播放。

要约 vs 应答

特征 要约 应答
​DTLS 角色​ a=setup:actpass(可以主动或被动) a=setup:active或 a=setup:passive(明确角色)
​编解码器列表​ 列出所有支持的编解码器 只包含双方都支持的编解码器子集
​ICE 凭证​ 生成新的 ice-ufrag 和 ice-pwd 生成自己的 ice-ufrag 和 ice-pwd
​媒体端口​ 可能是实际端口或占位符 确认最终使用的端口
邀约的格式

信令是 WebRTC 的灵魂。它的目的是让两个浏览器之间交换必要的网络和媒体信息。这些信息被封装在 ​​SDP​​ 中。
​​SDP 的格式是纯文本的​​,结构清晰,由多行 <type>=<value>的键值对组成。
一个典型的 Offer SDP 示例:

v=0 // SDP 版本号
o=- 8616478034590271513 2 IN IP4 127.0.0.1 // 会话源标识符
s=- // 会话名称
t=0 0 // 会话有效时间
a=group:BUNDLE 0 1 // 指示音频和视频流复用同一个传输通道
a=msid-semantic: WMS local-stream // 媒体流标识符// 音频媒体流描述
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
// ^ 媒体类型 ^ 端口(9 代表`discard`端口,实际端口由ICE决定) ^ 传输协议 ^ 支持的编解码器负载类型
c=IN IP4 0.0.0.0 // 连接信息(在ICE中通常无效)
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:khLS // ICE 用户名片段(用于连通性检查)
a=ice-pwd:CTc0134a304DhQZ1e2gjsdQz // ICE 密码
a=fingerprint:sha-256 FA:62:...:35 // DTLS 证书指纹,用于安全连接
a=setup:actpass // DTLS 角色,actpass 表示“我可以是客户端或服务端”
a=mid:0 // 媒体流ID,与BUNDLE对应
a=sendrecv // 媒体流方向:sendrecv(收发)、recvonly(只收)等
a=rtpmap:111 opus/48000/2 // 编解码器映射:负载类型111对应Opus编码
a=rtpmap:103 ISAC/16000
... // 更多编解码器参数// 视频媒体流描述
m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:khLS
a=ice-pwd:CTc0134a304DhQZ1e2gjsdQz
a=fingerprint:sha-256 FA:62:...:35
a=setup:actpass
a=mid:1
a=sendrecv
a=rtpmap:100 VP8/90000 // 编解码器映射:负载类型100对应VP8编码
a=rtpmap:101 VP9/90000
...
  1. ​会话级别信息​

    • v=0:SDP 版本号
    • o=:会话起源信息(用户名、会话ID、版本、网络类型、地址类型、地址)
    • s=-:会话名称(通常为"-")
  2. ​媒体描述块​​ - 每个 m=行开始一个媒体描述

    • m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104...:音频媒体行
      • audio:媒体类型
      • 9:端口号(在offer/answer中通常是占位符)
      • UDP/TLS/RTP/SAVPF:传输协议
      • 111 103 104...:支持的负载类型(对应下面的编解码器)
  3. ​连接数据​

    • c=IN IP4 0.0.0.0:连接信息(在offer中通常是0.0.0.0)
  4. ​ICE 参数​

    • a=ice-ufrag:khLS:ICE 用户名片段
    • a=ice-pwd:cx5ZZgLSQ3GQl8l+apE+Ys:ICE 密码
    • 这些用于验证 ICE 候选
  5. ​DTLS 参数​

    • a=fingerprint:sha-256 12:DF:3E...:证书指纹,用于安全连接
    • a=setup:actpass:DTLS 角色协商(actpass 表示可以充当客户端或服务器)
  6. ​媒体能力​

    • a=rtpmap:111 opus/48000/2:编解码器映射(负载类型111对应Opus编码,48kHz,2声道)
    • a=rtpmap:96 VP8/90000:视频编解码器VP8,时钟频率90kHz
    • a=sendrecv:媒体方向(发送和接收)
  7. ​编解码器参数​

    • a=fmtp:111 minptime=10;useinbandfec=1:Opus编码器的具体参数
应答的格式
v=0
o=- 4611731400430051337 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2
a=extmap-allow-mixed
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:Yf8o
a=ice-pwd:GEY91yL0qLdW3lq6a+5vVz
a=ice-options:trickle
a=fingerprint:sha-256 34:AB:CD:EF:56:78:90:12:34:56:78:90:...
a=setup:active
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=sendrecv
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
m=video 9 UDP/TLS/RTP/SAVPF 96 98
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:Yf8o
a=ice-pwd:GEY91yL0qLdW3lq6a+5vVz
a=ice-options:trickle
a=fingerprint:sha-256 34:AB:CD:EF:56:78:90:12:34:56:78:90:...
a=setup:passive
a=mid:1
a=sendrecv
a=rtcp-mux
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtpmap:98 VP9/90000

混合开发 RTSP 图传实现

WebRTC 播放

in short 规范没有定义信令的数据传输方式​, 建立P2P通信, 关键在于对接交换信令的过程, 定制开发即可!

  • ​A 创建邀约(offer)​​:A 调用 pc.createOffer()创建一个“邀约(offer)”,包含 A 的媒体能力。
  • ​A 设置本地描述​​:A 调用 pc.setLocalDescription(offer)将这个“邀约(offer)”设为自己的本地描述。
  • ​A 发送邀约(offer)​​:A 通过​​信令服务器​​(如 WebSocket)将这个“邀约(offer)”发送给 B。
  • ​B 设置远端描述​​:B 收到 A 的“邀约(offer)”后,调用 pc.setRemoteDescription(offer),告诉自己的连接对象“A 是这么说的”。
  • ​B 创建应答(answer)​​:B 调用 pc.createAnswer()创建一个“应答(answer)”,包含 B 的媒体能力。
  • ​B 设置本地描述​​:B 调用 pc.setLocalDescription(answer)
  • ​B 发送应答(answer)​​:B 通过​​信令服务器​​将“应答(answer)”发送给 A。
  • ​A 设置远端描述​​:A 收到 B 的“应答(answer)”后,调用 pc.setRemoteDescription(answer)
<!--* @Author: yangfh* @Date: 2025-10-11 10* @LastEditors: yangfh* @LastEditTime: 2025-10-11 10* @Description: * 
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebRTC 测试</title>
</head>
<body>
<h3>WebRTC 测试播放界面</h3>
<video id="video" autoplay playsinline controls></video><script>
let pc = new RTCPeerConnection();pc.ontrack = e => {document.getElementById("video").srcObject = e.streams[0];
};// 由Android传入offer
function onOffer(sdp) {pc.setRemoteDescription({ type: "offer", sdp }).then(() => pc.createAnswer()).then(answer => {pc.setLocalDescription(answer);// 回传 answer 给 Android WebRTCServerif (window.AndroidInterface) {window.AndroidInterface.onAnswer(answer.sdp);}});
}
</script>
</body>
</html>
package org.yang.webrtc.ffmpeg.make.webrtc;import android.content.Context;
import android.util.Log;import org.webrtc.*;import java.nio.ByteBuffer;
import java.util.Collections;public class WebRTCServer {private static final String TAG = "WebRTCServer";private PeerConnectionFactory factory;private PeerConnection peerConnection;private VideoTrack videoTrack;private SurfaceTextureHelper surfaceTextureHelper;private VideoSource videoSource;private EglBase eglBase;public interface SDPListener {void onLocalSDP(String sdp);}public WebRTCServer(Context context) {eglBase = EglBase.create();PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context).setEnableInternalTracer(true).createInitializationOptions());PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();factory = PeerConnectionFactory.builder().setOptions(options).setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)).setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())).createPeerConnectionFactory();videoSource = factory.createVideoSource(false);videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);videoTrack.setEnabled(true);}public void createPeer(SDPListener listener) {PeerConnection.RTCConfiguration rtcConfig =new PeerConnection.RTCConfiguration(Collections.emptyList());rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;peerConnection = factory.createPeerConnection(rtcConfig, new PeerConnection.Observer() {@Override public void onIceCandidate(IceCandidate candidate) {}@Overridepublic void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}@Override public void onAddStream(MediaStream stream) {}@Overridepublic void onRemoveStream(MediaStream mediaStream) {}@Overridepublic void onDataChannel(DataChannel dataChannel) {}@Overridepublic void onRenegotiationNeeded() {}@Overridepublic void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {}@Overridepublic void onSignalingChange(PeerConnection.SignalingState signalingState) {}@Overridepublic void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {}@Override public void onConnectionChange(PeerConnection.PeerConnectionState newState) {Log.i(TAG, "Connection: " + newState);}@Overridepublic void onIceConnectionReceivingChange(boolean b) {}@Overridepublic void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {}@Override public void onTrack(RtpTransceiver transceiver) {}});peerConnection.addTrack(videoTrack);peerConnection.createOffer(new SdpObserver() {@Override public void onCreateSuccess(SessionDescription sdp) {peerConnection.setLocalDescription(this, sdp);listener.onLocalSDP(sdp.description);}@Override public void onSetSuccess() {}@Override public void onCreateFailure(String s) { Log.e(TAG, s); }@Override public void onSetFailure(String s) {}}, new MediaConstraints());}public void setRemoteSDP(String sdp) {SessionDescription remote = new SessionDescription(SessionDescription.Type.ANSWER, sdp);peerConnection.setRemoteDescription(new SdpObserver() {@Override public void onSetSuccess() { Log.i(TAG, "Remote SDP set"); }@Override public void onSetFailure(String s) { Log.e(TAG, s); }@Override public void onCreateSuccess(SessionDescription sessionDescription) {}@Override public void onCreateFailure(String s) {}}, remote);}/***// TODO 待实现, 貌似使用MSE api 更加简单//  * 从 FFmpeg 拿到的是 压缩 H.264 NALU。//  * WebRTC 的 VideoSource 接口只接受原始帧(I420 或 NV12)//  * 模拟VideoFrame推送接口// 推送H264帧到WebRTC视频流* @param data* @param timestampMs*/public void pushH264Frame(byte[] data, long timestampMs) {//        ByteBuffer buffer = ByteBuffer.wrap(data);
//        VideoFrame.I420Buffer dummyBuffer = JavaI420Buffer.allocate(1, 1);
//        VideoFrame frame = new VideoFrame(dummyBuffer, 0, timestampMs * 1000);
//        videoSource.getCapturerObserver().onFrameCaptured(frame);
//        frame.release();}public void release() {if (peerConnection != null) peerConnection.close();if (factory != null) factory.dispose();if (eglBase != null) eglBase.release();}
}

fun switchSDP(){val ffmpeg = FFmpegInterface()ffmpeg.ffmpegInit();val webrtcServer = WebRTCServer(this)webrtcServer.createPeer { sdp: String? ->Log.i("switchSDP", "onCreatePeer SDP 的内容是: $sdp")mHandler.post{// SDP 发给 WebViewwebView.evaluateJavascript("onOffer(" + JSONObject.quote(sdp) + ")", null)}}ffmpeg.startRtspPullById("cam01", "rtsp://admin:sny123.com@192.168.20.80:554/h264/ch1/main/av_stream" ) { data: ByteArray?, pts: Int, dts: Int ->webrtcServer.pushH264Frame(data,pts.toLong())}}

从 FFmpeg 拿到的是 压缩的 H.264 NALU。

原始 H.264 NALU 数据​​不能直接​​通过 WebRTC 的 RTCDataChannel或直接作为媒体流发送。它们必须被封装成符合 RFC 6184 规范的 RTP 包。

将 Annex B 格式的 NALU 数据​​按照 RFC 6184 规则封装成 RTP 包​​。包括:

  • 添加 RTP 头部(版本、填充位、扩展位、CSRC计数、标记位、序列号、时间戳、SSRC)。
  • 根据 NALU 大小决定使用 Single NALU Mode 还是 FU-A Mode。
  • 构造 FU-A 分片头或 STAP-A 聚合头。
  • 处理 SPS/PPS

贼麻烦 也可以使用MSE:

MSE 播放

使用MSE:

  • Android NDK 用 FFmpeg 拉流(RTSP → H264 NALU)
  • 通过 WebSocket 推送 H264 数据片段(fMP4 格式)
  • Web 端用 Media Source Extensions (MSE) 播放

前端 MSE

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="description" content="jMuxer - a simple javascript mp4 muxer for non-standard streaming communications protocol"><meta name="keywords" content="h264 player, mp4 player, mse, mp4 muxing, jmuxer, aac player"><title> stream demo</title></head>
<body>
<div id="container" style="margin: 0 auto; text-align: center;">	<div style="padding: 5px;"><button id="init_but" onclick="init()">init</button><button id="player_but" onclick="player()">player</button><button onclick="fb()">-1S</button><button onclick="fs()">+1S</button><button onclick="debugg()">last</button></div><video style="border: 1px solid #333; max-width: 600px;height: 400px;"  controls autoplay id="player" muted></video></div>
<script>var chunks = [];
var video = document.getElementById('player');
function init() {console.log("=============== init ===============");document.getElementById('init_but').disabled = true;window.mse = new (MediaSource || WebKitMediaSource)();window.sourceBuffer;video.src = URL.createObjectURL(window.mse);video.autoplay = true;video.playsInline = true;video.muted = true;mse.addEventListener('sourceopen', onMediaSourceOpen);
}function onMediaSourceOpen() {sourceBuffer = mse.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');sourceBuffer.timestampOffset = 0;sourceBuffer.appendWindowStart = 0;sourceBuffer.appendWindowEnd = Infinity;sourceBuffer.addEventListener('updateend', addMoreBuffer);video.play();
}
function addMoreBuffer() {if (sourceBuffer.updating || !chunks.length) {return;}sourceBuffer.appendBuffer(chunks.shift());
}window.onload = function() {init();player();
};function player(){var socketURL = 'ws://127.0.0.1:19909';//var socketURL = 'ws://localhost:3265/open/api/we';var ws = new WebSocket(socketURL);ws.binaryType = 'arraybuffer';ws.addEventListener('message',function(event) {chunks.push(new Uint8Array(event.data));addMoreBuffer();});ws.addEventListener('error', function(e) {console.log('Socket Error');});document.getElementById('player_but').disabled = true;}
function fs(){video.currentTime = video.currentTime+1
}
function fb(){video.currentTime = video.currentTime-1
}
function debugg(){console.log(video)video.currentTime = sourceBuffer.buffered.end(0)
}
</script>
</body>
</html>

低延迟 fmp4

两个地方

一个是 打开 RTSP 输入时

    // --- 打开 RTSP ---av_dict_set(&opts, "rtsp_transport", "tcp", 0);av_dict_set(&opts, "fflags", "nobuffer+flush_packets", 0);av_dict_set(&opts, "max_delay", "100000", 0);//微秒 0.1sav_dict_set(&opts, "flags", "low_delay", 0);
//    av_dict_set(&opts, "stimeout", "500000", 0); // 0.5sif (avformat_open_input(&in_fmt, ctx->url.c_str(), nullptr, &opts) < 0) {LOGE("Failed to open RTSP: %s", ctx->url.c_str());cleanup();return;}

一个是 设置 out 的 AVFormatContext 时

// --- 设置 fMP4 分片输出参数 ---av_dict_set(&muxOpts, "movflags", "frag_keyframe+empty_moov+default_base_moof+dash", 0);av_dict_set(&muxOpts, "flush_packets", "1", 0);av_dict_set(&muxOpts, "min_frag_duration", "0", 0);av_dict_set(&muxOpts, "frag_duration", "200000", 0);//分片时长微秒, 这个太低会转流时崩溃!....// --- 写头部(发送 init segment)---if (avformat_write_header(out_fmt, &muxOpts) < 0) {LOGE("avformat_write_header failed");cleanup();return;}while (ctx->running) {int ret = av_read_frame(in_fmt, pkt);if (ret < 0) {break;}..........pkt->stream_index = out_stream->index;if (read_frame_count< 100 ) {LOGI("[pkt %ld] stream=%s,地址:0x%" PRIxPTR ", pts=%" PRId64 ", dts=%" PRId64 ", size=%d, key=%d, dur=%" PRId64 ", pos=%" PRId64,push_frame_count,pkt->stream_index==videoIndex?"video":"audio",(uintptr_t)pkt,pkt->pts,pkt->dts,pkt->size,(pkt->flags & AV_PKT_FLAG_KEY) ? 1 : 0,pkt->duration,pkt->pos);}
//        ret = av_interleaved_write_frame(out_fmt, pkt);ret = av_write_frame(out_fmt, pkt);//转封装格式av_packet_unref(pkt);}....

Fatal signal 11 (SIGSEGV) crash?

A Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xb07bebdc15b034 in tid 9650 (RenderThread), pid 9631 (rtc.ffmpeg.make)

document.getElementById('init_but').disabled = true;
window.mse = new (MediaSource || WebKitMediaSource)();
window.sourceBuffer;
video.src = URL.createObjectURL(window.mse);//加载渲染
video.autoplay = true;
video.playsInline = true;
video.muted = true;

FFmpeg 转封格式 av_write_frame(out_fmt, pkt); 叠加前端 video.src = URL.createObjectURL(window.mse);//视频渲染 会导致奇妙的化学反应 渲染线程指针越界? app崩溃, 系统级问题. 离大谱, 遂换个方式解决 直接找个能播放裸流的播放器..

EasyPlayer 播放(裸流)

EasyPlayer - https://github.com/EasyDarwin/EasyPlayer.js

使用 EasyPlayer 可以配置播放H264裸流, 无需ffmpeg转封装格式!!

EasyPlayer

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>演示</title><script src="./js/EasyPlayer-pro.js"></script>
</head>
<style>.player_item {position: relative;padding-bottom: 56%;background-color: black;margin-bottom: 20px;}.player_box {position: absolute;top: 0;bottom: 0;right: 0;left: 0;}.btn-item {cursor: pointer;display: inline-block;padding: 6px 12px;margin-right: 15px;border-radius: 4px;border: 1px #ccc solid;}.df {display: flex;align-items: center;margin-bottom: 16px;}.box {margin: auto;max-width: 800px;}
</style><body><div class="box"><div class="player_item"><div class="player_box" id="player_box"></div></div><div><!-- <input id="hasAudio" type="checkbox" /><span >音频</span> --></div><input class="inputs" type="text" id="input" value="ws://127.0.0.1:19909"><div><div class="btn-item" id="onPlayer" >播放</div><div class="btn-item" id="onReplay" >重播</div><div class="btn-item" id="onMute">静音</div><div class="btn-item" id="onStop" >注销</div></div></div><script>window.onload = function () {var playerInfo = nullvar config = {isLive:true,hasAudio:false,isMute:false,MSE:false,//MSE 不稳定, 卡画面isFlow:true,loadTimeOut:10,bufferTime:-1,webGPU:false,//webGPU 不稳定, 卡画面canvasRender:true,gpuDecoder:true,stretch: false}playCreate()var input = document.getElementById('input');var player = document.getElementById('onPlayer');var replay = document.getElementById('onReplay');var mute = document.getElementById('onMute');var stop = document.getElementById('onStop');player.onclick = () => {onPlayer()}replay.onclick = () => {onReplay()}mute.onclick = () => {onMute()}stop.onclick = () => {onStop()}function playCreate() {var container = document.getElementById('player_box');playerInfo = new EasyPlayerPro(container, config);}function onPlayer() {playerInfo && playerInfo.play(input.value).then(() => {}).catch((e) => {console.error(e);});}function onMute() {if (!playerInfo)returnplayerInfo.setMute(true)}function onReplay() {onDestroy().then(() => {playCreate();onPlayer()});}function onDestroy() {return new Promise((resolve, reject) => {if (playerInfo) {playerInfo.destroy()playerInfo = null}setTimeout(() => {resolve();}, 100);})}function onStop() {onDestroy().then(() => {playCreate();});}//自动播放setTimeout(() => { onPlayer() }, 1500);}</script>
</body></html>

FFmpeg 转流

#include <jni.h>
#include <string>
#include <thread>
#include <map>
#include <mutex>
#include <atomic>
#include <android/log.h>extern "C" {#include <libavformat/avformat.h>#include <libavcodec/avcodec.h>#include <libavutil/avutil.h>#include <libavutil/time.h>
}#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "RTSP_FFMPEG", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "RTSP_FFMPEG", __VA_ARGS__)static JavaVM *g_vm = nullptr;struct StreamContext {std::string id;std::string url;std::thread thread;std::atomic<bool> running{false};jobject wsServer; // WebSocket 推送服务
};static std::map<std::string, StreamContext*> g_streams;
static std::mutex g_mutex;static void rtspThread(StreamContext *ctx);// 输出到文件 调试用
static FILE*  init_file(const char* output_filename) {// 打开输出文件FILE* output_file = fopen(output_filename, "ab");  // 追加二进制模式if (!output_file) {LOGE("Could not open output file");return nullptr;}return output_file;
}extern "C"
JNIEXPORT void JNICALL
Java_org_yang_webrtc_ffmpeg_make_ffmpeg_FFmpegInterface_startRtspToWs(JNIEnv *env, jobject thiz,jstring jid, jstring jurl, jobject ws_server) {const char *id = env->GetStringUTFChars(jid, nullptr);const char *url = env->GetStringUTFChars(jurl, nullptr);std::lock_guard<std::mutex> lock(g_mutex);if (g_streams.find(id) != g_streams.end()) {LOGI("Stream [%s] already exists", id);env->ReleaseStringUTFChars(jid, id);env->ReleaseStringUTFChars(jurl, url);return;}LOGI("FFMPEG VERSION [%s] ", av_version_info());StreamContext *ctx = new StreamContext();ctx->id = id;ctx->url = url;ctx->running = true;env->GetJavaVM(&g_vm);ctx->wsServer = env->NewGlobalRef(ws_server);g_streams[id] = ctx;ctx->thread = std::thread(rtspThread, ctx);LOGI("Start RTSP pull id=%s, url=%s", id, url);env->ReleaseStringUTFChars(jid, id);env->ReleaseStringUTFChars(jurl, url);
}static void rtspThread(StreamContext *ctx) {JNIEnv *env = nullptr;g_vm->AttachCurrentThread(&env, nullptr);jclass wsCls = env->GetObjectClass(ctx->wsServer);jmethodID broadcastFrame = env->GetMethodID(wsCls, "broadcastFrame", "([B)V");
//    jmethodID broadcastFrame = env->GetMethodID(wsCls, "broadcastFrame", "(Ljava/nio/ByteBuffer;)V");avformat_network_init();AVFormatContext *fmt_ctx = nullptr;AVDictionary *opts = nullptr;av_dict_set(&opts, "rtsp_transport", "tcp", 0);av_dict_set(&opts, "stimeout", "5000000", 0);
//    av_dict_set(&opts, "rtsp_transport", "tcp", 0);
//    av_dict_set(&opts, "fflags", "nobuffer+flush_packets", 0);
//    av_dict_set(&opts, "max_delay", "100000", 0);//微秒 0.1s
//    av_dict_set(&opts, "flags", "low_delay", 0);int ret = avformat_open_input(&fmt_ctx, ctx->url.c_str(), nullptr, &opts);av_dict_free(&opts);if (ret < 0 || !fmt_ctx) {LOGE("Failed to open RTSP: %s", ctx->url.c_str());env->DeleteGlobalRef(ctx->wsServer);g_vm->DetachCurrentThread();return;}if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {LOGE("Failed to find stream info");avformat_close_input(&fmt_ctx);env->DeleteGlobalRef(ctx->wsServer);g_vm->DetachCurrentThread();return;}int videoIndex = -1;for (unsigned int i = 0; i < fmt_ctx->nb_streams; ++i) {if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {videoIndex = i;break;}}if (videoIndex == -1) {LOGE("No video stream found");avformat_close_input(&fmt_ctx);env->DeleteGlobalRef(ctx->wsServer);g_vm->DetachCurrentThread();return;}AVPacket *pkt = av_packet_alloc();if (!pkt) {LOGE("Failed to allocate packet");avformat_close_input(&fmt_ctx);env->DeleteGlobalRef(ctx->wsServer);g_vm->DetachCurrentThread();return;}LOGI("Start reading RTSP stream [%s]", ctx->id.c_str());//调试输出
//    char debug_filename[] = "/sdcard/Download/stream_video_a001.264";
//    FILE* output_file = init_file(debug_filename);long read_frame_count = 0;long pust_frame_count = 0;while (ctx->running) {ret = av_read_frame(fmt_ctx, pkt);if (ret < 0) {LOGE("Read frame error or stream ended");break;}read_frame_count++;if (pkt->stream_index == videoIndex && pkt->size > 0) {jbyteArray arr = env->NewByteArray(pkt->size);if (arr != nullptr) {long startTime = av_gettime();env->SetByteArrayRegion(arr, 0, pkt->size, reinterpret_cast<const jbyte*>(pkt->data));env->CallVoidMethod(ctx->wsServer, broadcastFrame, arr);env->DeleteLocalRef(arr);pust_frame_count++;//LOGI("推送 %ld 帧,  调用耗时 %ld  ", pust_frame_count, av_gettime() - startTime);}if (read_frame_count< 100 ) {LOGI("[pkt %ld] stream=%s,地址:0x%" PRIxPTR ", pts=%" PRId64 ", dts=%" PRId64 ", size=%d, key=%d, dur=%" PRId64 ", pos=%" PRId64,read_frame_count,pkt->stream_index==videoIndex?"video":"audio",(uintptr_t)pkt,pkt->pts,pkt->dts,pkt->size,(pkt->flags & AV_PKT_FLAG_KEY) ? 1 : 0,pkt->duration,pkt->pos);}if (pkt->stream_index==videoIndex  &&(pkt->flags & AV_PKT_FLAG_KEY) ) {LOGI("[%s] Keyframe [%ld] ", ctx->id.c_str(), pust_frame_count);}
//            //调试输出
//            size_t written = fwrite(pkt->data, 1, pkt->size, output_file);
//            if (written != static_cast<size_t>(pkt->size)) {
//                LOGE("Write error: wrote %zu of %d bytes", written, pkt->size);
//            }}if (read_frame_count % 50 == 0){LOGI("已接受 %ld 帧, 已推送 %ld 帧, size %d", read_frame_count, pust_frame_count, pkt->size);}av_packet_unref(pkt);}LOGI("RTSP thread [%s] stopping", ctx->id.c_str());av_packet_free(&pkt);avformat_close_input(&fmt_ctx);env->DeleteGlobalRef(ctx->wsServer);g_vm->DetachCurrentThread();
}extern "C"
JNIEXPORT void JNICALL
Java_org_yang_webrtc_ffmpeg_make_ffmpeg_FFmpegInterface_stopRtspToWs(JNIEnv *env, jobject, jstring jid) {const char *id = env->GetStringUTFChars(jid, nullptr);std::lock_guard<std::mutex> lock(g_mutex);auto it = g_streams.find(id);if (it == g_streams.end()) {LOGE("Stop: Stream [%s] not found", id);env->ReleaseStringUTFChars(jid, id);return;}StreamContext *ctx = it->second;ctx->running = false;if (ctx->thread.joinable()) ctx->thread.join();delete ctx;g_streams.erase(it);LOGI("Stopped stream [%s]", id);env->ReleaseStringUTFChars(jid, id);
}

注意一点, 推给前端的数据, 若没有开始码, 可以插入NALU的开始码.

//会被 native 调用 推送帧public void broadcastFrame(byte[] data) {synchronized (this){if (receiveCount%100 == 0)Log.i(TAG, "broadcastFrame: thread= "+ Thread.currentThread().getId()+", data size=" + data.length+", clients=" + clients.size()+", receiveCount: " +receiveCount);receiveCount++;for (Client c : clients) {try {byte[]startCode = {0x00,0x00,0x00,0x01};byte[] newData = new byte[data.length+4];System.arraycopy(startCode,0,newData,0,4);System.arraycopy(data,0,newData,4,data.length);c.send(newData);} catch (Exception e) {Log.e(TAG, "Send failed: " + e.getMessage());}}if (receiveCount% 300== 0)Log.i(TAG, "receiveCount: " +receiveCount);}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/963730.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

语法记录

\(\texttt{titile}\) 计概与数算行末不要空格!!! 字符串与字符数组读入 cin >> str //字符与字符串,不含空格、制表符与换行 scanf("%c", &ch) //读单个字符 ch scanf(" %c", &…

Can Large Language Models Detect Rumors on Social Media?

论文信息论文标题:Can Large Language Models Detect Rumors on Social Media?论文作者:刘强、陶翔、吴俊飞、吴舒、王亮论文来源:发布时间:2024-02-06论文地址:link论文代码:link1 介绍 1.1 研究动机研究动机围…

压迫

从明天开始到考研结束前 拒绝三角洲 方舟想起来了挂一下,没想起来就算了 抖音不再打开 b站用来看课 每天总结反思 多学多想多思考 提高效率 瞄着最高分努力 看看实力 如果考的太高完全可以选择二战,这也是二战得信心…

P13573 [CCPC 2024 重庆站] Pico Park

P13573 [CCPC 2024 重庆站] Pico Park 题意: 游戏中,有 \(n\le 500\) 名玩家,依次站在数轴的 \(1,2,3, \dots, n\) 处,第 \(i\) 名玩家有一个面向的方向 \(d_i\),为向左或向右。 每名玩家手里有一把缩小枪,玩家会…

手工安装gcc-13.3.0

手工安装gcc-13.3.0wget https://gcc.gnu.org/pub/gcc/infrastructure/gmp-6.2.1.tar.bz2wget https://gcc.gnu.org/pub/gcc/infrastructure/mpfr-4.1.0.tar.bzwget https://gcc.gnu.org/pub/gcc/infrastructure/mpc-1…

深入解析:Cookie、Session、JWT、SSO,网站与 APP 登录持久化与缓存

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

gowin ide linux安装教程

1.下载,下载地址 https://www.gowinsemi.com.cn/software/index 我下载的版本是Gowin_V1.9.11.03_Education_Linux ps:云源软件和云源编程器的区别: 云源软件包含一系列工具如ide,综合工具,编程器等等 云源编程器就…

AT_arc111_f [ARC111F] Do you like query problems?

首先这个取 \(\min,\max\) 操作很不好做。 并且对可能的操作序列计数很不好做。 参考 【题解】ARC111F Do you like query problems?。 考虑先转期望,求出每种操作序列的期望结果。 发现序列中每个位置都不受其他位置…

Win7 隐藏文件夹盘符

1、打开注册表编辑器 regedit 2、找到 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer 3、 右键新建 DWORD-32 值:名称修改为:NoDrives 十进制值按下表填写4、重启系统生效!

pythontip 按条件过滤字典

编写一个程序,根据某个条件过滤字典值。 对于这个挑战,条件是字典值应该大于整数k。 定义函数filter_dict_values(),有两个参数:字典mixed_dict和整数k。 在函数内部,创建一个新字典,并从mixed_dict过滤值不是整…

DotNetGuide 突破了 9.5K + Star,一份全面的C#/.NET/.NET Core学习、工作、面试指南知识库!

DotNetGuide 介绍 DotNetGuide是一个专注于C#/.NET/.NET Core学习、工作、面试指南的 GitHub 知识库,该知识库记录、收集和总结了C#/.NET/.NET Core基础知识、学习路线、开发实战、编程技巧练习、学习视频、文章、书籍…

如何把华为mate 60手机备份到移动硬盘

如何把mate 60备份到移动硬盘华为Mate 60可通过USB OTG连接移动硬盘进行数据备份,以下是具体操作步骤: 连接移动硬盘使用USB OTG数据线将移动硬盘连接至Mate 60的Type-C接口。 确保移动硬盘已插入电源(部分硬盘需额…

Vue实例学习

vue的学习 1.初始化渲染 第一步构建用户界面,第二步创建vue实例初始化渲染 核心步骤分为4步: 1.准备容器,假设这个容器的唯一id是app 2.引包(vue的版本),vue有两种版本开发版本/生产版本 开发版本有完整的警告和…

2.2 语言处理程序基础

2.2 语言处理程序基础2.2 语言处理程序基础以编译方式翻译C/C++源程序的过程中,类型检查在( )阶段处理。 A. 词法分析 B. 语义分析 C. 语法分析 D. 目标代码生成正确答案是 B。 解析 本题考查的是编译器工作过程。…

Ai元人文:价值的“迷思”与“归真”——从家庭之爱到文明共生

Ai元人文:价值的“迷思”与“归真”——从家庭之爱到文明共生 ——Ai元人文构想 价值的“迷思”与“归真”:从家庭之爱到文明共生 第一章:困局——迷失在词语的丛林 我们生活在一个价值词汇泛滥的时代。“公平”、“…

MATLAB 数据可视化教程:从基础到进阶

MATLAB 数据可视化教程:从基础到进阶以下是一个全面的 MATLAB 数据可视化教程,涵盖从基础绘图到进阶技巧的内容: 1. 基础绘图函数 MATLAB 提供了多种基础绘图函数,适用于不同类型的数据展示。 1.1 二维线图 (plot)…

在ec2上部署qwen3-VL-2B模型

测试环境如下 g5.4xlarge EBS: 200GB AMI:ami-0a83c884ad208dfbc ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-20250419安装nvidia驱动和cuda toolkit 查看PCIE设备性能参数参考,https://www.nvidia…

37

所学时间:9小时 博客量1

【数据结构】第六章启航:图论入门——从零掌握有向图、无向图与简单图

【数据结构】第六章启航:图论入门——从零掌握有向图、无向图与简单图【数据结构】第六章启航:图论入门——从零掌握有向图、无向图与简单图 导读大家好,很高兴又和大家见面啦!!!【数据结构】这门课主要会学习2种…

日总结 26

Git 是由 Linus Torvalds 于 2005 年开发的分布式版本控制系统,核心特点包括分布式架构(本地仓库含完整历史,支持离线工作)、高效快照式版本跟踪、低成本强功能分支管理、SHA-1 哈希保障的数据完整性及非线性开发支…