Android投屏Demo实战:MediaProjection采集与H264硬编码全链路解析
简介这份Android设备投屏功能Demo面向具备一定Android基础的开发者聚焦手机、平板屏幕内容实时投放到电视或电脑显示器这一常见需求。项目以MediaProjection捕获屏幕帧借助MediaCodec完成H264编解码并通过Socket在设备间传输视频流接收端再解码还原画面完整串联了录屏、编码、网络通信与解码显示的关键链路。资源包共1536个文件以975个flat、228个xml、218个json等构建与配置产物为主另有36个java源码、14个png及若干gradle、jar文件压缩包约4.87MB目录结构便于按模块查阅。目前已有3256人学习下载。读者可从中掌握MediaProjectionManager权限申请、MediaCodec缓冲区填充与输出提取、ServerSocket连接建立及NAL单元解析等实现细节并据此扩展音视频同步、优化网络传输效率适合作为Android多媒体与远程投屏方向的学习参考。1. 从一块黑屏说起Android 投屏 Demo 到底在解决什么问题手机连上大屏画面却卡成 PPT或者干脆黑屏只出声——这类问题我见过太多次。很多人第一反应是线材不行、电视太老其实根子往往在编码链路采集到的原始帧没被正确喂给编码器或者编码器吐出的 H264 码流在接收端解不出来。这份 Android 设备投屏功能 Demo 要解决的就是把「屏幕采集 → 硬编码 → 推流 → 解码显示」这条链路完整跑通让你手里有一个能改、能调、能定位问题的可运行骨架。它适合两类人一是想给自己的 App 加投屏能力但不知道从哪下手的 Android 开发二是做音视频方向、需要一份能对照排查 H264 编码参数的学生或初级工程师。Demo 本身不追求低延迟到极致但每个环节都留了可观测的接口这点比很多「能跑但黑盒」的示例强。2. 投屏链路拆解MediaProjection 采集与 H264 硬编码怎么接投屏的本质是把屏幕像素变成另一台设备能播放的视频流。Android 侧的标准入口是 MediaProjection它拿到的是屏幕合成的原始帧格式通常是 RGBA 或 YUV。原始帧体积巨大1080p 一帧就接近 8MB不编码根本没法传。所以链路里必须有一个 H264 编码器把原始帧压成几十 KB 的码流。Demo 用的是 MediaCodec 硬编码而不是软编原因很直接硬编走 GPU/DSP功耗低、延迟小软编在手机上跑 1080p 很容易把 CPU 吃满导致掉帧。2.1 申请权限与创建 VirtualDisplayMediaProjection 不是普通权限必须走系统弹窗授权。拿到 resultCode 和 data 之后才能创建 VirtualDisplay把屏幕内容「投影」到一个 Surface 上。这个 Surface 就是编码器的输入源。// 在 Activity 中发起投屏授权 MediaProjectionManager mpm (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE); Intent intent mpm.createScreenCaptureIntent(); startActivityForResult(intent, REQ_CODE); // onActivityResult 中拿到授权结果 Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode REQ_CODE resultCode RESULT_OK) { MediaProjection projection mpm.getMediaProjection(resultCode, data); // 创建编码器输入用的 Surface Surface inputSurface mediaCodec.createInputSurface(); // 把屏幕内容投射到这个 Surface VirtualDisplay vd projection.createVirtualDisplay( ScreenCast, // 名称调试时能看到 width, height, // 建议用屏幕真实分辨率 densityDpi, // 屏幕密度 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, inputSurface, null, null); } }这里有个容易忽略的点width 和 height 最好取屏幕真实分辨率不要随手写 1280x720。VirtualDisplay 会按你给的分辨率缩放如果和编码器配置不一致画面会被拉伸或裁切。densityDpi 同理填错会导致内容显示比例异常。授权回调里拿到的 data 只能使用一次重复 getMediaProjection 会抛异常所以 projection 对象要保存好后续停止投屏时调用 release。2.2 配置 MediaCodec 编码器编码器配置决定了输出码流的规格。Demo 里用 H264 的 baseline 或 main profile具体看设备支持。关键参数有四个分辨率、帧率、码率、关键帧间隔。分辨率跟 VirtualDisplay 保持一致帧率一般 30 够用60 对性能要求高码率 1080p 建议 4~8 Mbps太低会糊太高网络扛不住关键帧间隔I 帧间隔设 1~2 秒太短码率浪费太长接收端花屏恢复慢。MediaFormat format MediaFormat.createVideoFormat(video/avc, width, height); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // 必须用 Surface 模式 format.setInteger(MediaFormat.KEY_BIT_RATE, 6_000_000); // 6 Mbps format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1 秒一个 I 帧 MediaCodec codec MediaCodec.createEncoderByType(video/avc); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); Surface inputSurface codec.createInputSurface(); codec.start();COLOR_FORMAT 必须选 COLOR_FormatSurface因为我们是把 VirtualDisplay 的输出直接喂给编码器走的是 Surface 直通不经过 CPU 拷贝。如果选成 YUV420 之类的格式createInputSurface 会失败。这一点在官方文档里写得比较隐晦实际调试时如果 configure 报错先检查这个参数。2.3 取出码流并封装编码器开始工作后需要循环从 output buffer 里取数据。取出来的是 H264 裸流每个 buffer 可能包含一个或多个 NAL 单元。Demo 里为了简单直接把裸流通过 socket 发出去接收端按 Annex-B 格式起始码 00 00 00 01解析。MediaCodec.BufferInfo info new MediaCodec.BufferInfo(); while (isRunning) { int outIndex codec.dequeueOutputBuffer(info, 10_000); // 等 10ms if (outIndex 0) { ByteBuffer buffer codec.getOutputBuffer(outIndex); byte[] data new byte[info.size]; buffer.get(data); // 这里 data 就是 H264 裸流可以推流或存文件 sendData(data); codec.releaseOutputBuffer(outIndex, false); } else if (outIndex MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 格式变化时通常会带 SPS/PPS接收端需要这个才能解码 MediaFormat newFormat codec.getOutputFormat(); // 可以把 csd-0 / csd-1 取出来发给接收端 } }注意 INFO_OUTPUT_FORMAT_CHANGED 这个分支。H264 解码需要 SPS 和 PPS 参数集它们通常在格式变化时通过 csd-0 和 csd-1 给出。如果接收端只拿到普通帧没有 SPS/PPS解码器初始化会失败表现就是黑屏。常见做法是在格式变化时把 csd 数据单独发一包或者把 SPS/PPS 拼在每个 I 帧前面。3. 接收端解码与显示从 H264 裸流到画面发送端把码流推出去之后接收端要能解出来。Demo 的接收端可以是一个简单的 Android 应用也可以是一个桌面程序。核心逻辑是拿到 H264 数据 → 喂给解码器 → 拿到 YUV 或 RGB 帧 → 渲染到 SurfaceView 或 TextureView。这里最容易翻车的地方是帧边界处理因为 socket 是流式的一次 recv 可能拿到半帧也可能拿到两帧半。3.1 用 MediaCodec 做硬解码接收端同样用 MediaCodec配置成解码器。关键是要把 SPS/PPS 先喂进去否则 configure 会报错。如果发送端没有单独发 SPS/PPS可以在收到第一个 I 帧时从码流里提取。MediaFormat format MediaFormat.createVideoFormat(video/avc, width, height); // 如果发送端给了 csd直接设置 format.setByteBuffer(csd-0, spsBuffer); format.setByteBuffer(csd-1, ppsBuffer); MediaCodec decoder MediaCodec.createDecoderByType(video/avc); decoder.configure(format, outputSurface, null, 0); // outputSurface 来自 SurfaceView decoder.start(); // 收到数据后喂给解码器 int inIndex decoder.dequeueInputBuffer(10_000); if (inIndex 0) { ByteBuffer inBuf decoder.getInputBuffer(inIndex); inBuf.clear(); inBuf.put(h264Data); decoder.queueInputBuffer(inIndex, 0, h264Data.length, pts, 0); } // 取解码后的帧 int outIndex decoder.dequeueOutputBuffer(info, 10_000); if (outIndex 0) { decoder.releaseOutputBuffer(outIndex, true); // true 表示渲染到 Surface }outputSurface 一般从 SurfaceView 的 SurfaceHolder 拿。如果 Surface 还没创建好就 configure会抛 IllegalStateException所以要在 surfaceCreated 回调之后再初始化解码器。另外 pts 时间戳要单调递增随便填 0 可能导致渲染节奏异常。3.2 处理粘包与半包TCP 是字节流没有消息边界。发送端一次 send 的 10KB 数据接收端可能分三次 recv 才收完。如果直接把每次 recv 的数据当一帧喂给解码器大概率解不出来。常见做法是自定义一个简单协议每帧前面加 4 字节长度头。# 接收端伪代码按长度头拆帧 buffer b while True: data sock.recv(4096) buffer data while len(buffer) 4: frame_len int.from_bytes(buffer[:4], big) if len(buffer) 4 frame_len: break # 半包继续收 frame buffer[4:4frame_len] feed_decoder(frame) buffer buffer[4frame_len:]这个逻辑看着简单但实际写的时候容易在 buffer 拼接和切片上出错。血泪经验是每次循环都要重新判断 buffer 长度不要用固定偏移量。另外 frame_len 如果异常大比如超过 10MB说明流已经乱了直接断开重连比继续解析更省事。3.3 渲染与延迟控制解码出来的帧通过 releaseOutputBuffer(index, true) 直接渲染到 Surface。如果发现画面延迟越来越大通常是解码器输出队列堆积了。可以在每次 release 之后检查队列深度必要时丢帧。另一个影响延迟的因素是编码端的 B 帧B 帧需要缓存后续帧才能解码会增加延迟。投屏场景建议编码器关掉 B 帧或者用 baseline profile它本身不支持 B 帧。4. 避坑排查投屏 Demo 最容易翻车的五个地方这个 Demo 跑通不难但要在不同设备上稳定运行下面这几个坑基本都会遇到。我按「现象 → 原因 → 解决」整理方便对照排查。现象一授权后画面全黑但日志显示编码器在出帧。原因通常是 VirtualDisplay 的 Surface 和编码器的 inputSurface 不是同一个或者 createVirtualDisplay 时 flag 设错。解决确认 createVirtualDisplay 的第五个参数传的是 codec.createInputSurface() 返回的对象flag 用 VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR。现象二接收端解码报错提示「no SPS/PPS」。原因是发送端只发了普通帧没有把 csd-0/csd-1 传过去。解决在 INFO_OUTPUT_FORMAT_CHANGED 时把 csd 数据单独发一包接收端收到后先 configure 解码器再喂普通帧。现象三画面能显示但颜色不对偏绿或偏紫。这是 YUV 和 RGB 转换时的色彩空间问题。Android 硬编硬解一般走 NV12 或 I420如果中间某一步用了错误的转换矩阵颜色就会偏。解决尽量走 Surface 直通避免 CPU 侧做 YUV 转换如果必须转确认用的是 BT.601 还是 BT.709 矩阵两者不能混用。现象四投屏几秒后卡死编码器 dequeueOutputBuffer 一直返回 -1。常见原因是输入 Surface 没有数据进来VirtualDisplay 被系统回收了。部分厂商 ROM 在应用切后台时会自动停止投屏。解决投屏期间保持前台服务或者申请前台权限另外检查是否在 onPause 里误调了 projection.stop()。现象五不同手机分辨率适配混乱画面被拉伸。原因是编码分辨率写死了 1280x720但 VirtualDisplay 用了屏幕真实分辨率。解决统一用屏幕真实分辨率创建 VirtualDisplay 和编码器接收端按码流里的 SPS 解析出真实宽高再渲染不要写死。5. 进阶技巧用码率自适应和 I 帧请求稳住弱网投屏Demo 跑通之后下一步通常是让它能在网络波动时也不崩。这里分享两个我实际调过的技巧码率自适应和 I 帧请求。码率自适应的思路是接收端定期统计丢包率和缓冲深度通过一个反向通道告诉发送端调整编码码率。MediaCodec 支持动态改码率不需要重启编码器。// 发送端收到码率调整指令后 Bundle params new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitrate); codec.setParameters(params);newBitrate 不要一次调太猛比如从 6Mbps 直接降到 1Mbps画面会糊得没法看。我一般按 20% 步进调整每 2 秒调一次给编码器一个缓冲期。接收端统计周期也别太短500ms 一次就够了太频繁反而震荡。I 帧请求是另一个实用手段。当接收端发现花屏或者刚加入时可以主动向发送端请求一个 I 帧快速恢复画面。发送端收到请求后通过 setParameters 触发关键帧。Bundle params new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0); codec.setParameters(params);这个调用不是所有设备都支持部分 ROM 会忽略。如果发现请求无效可以退而求其次在发送端维护一个计数器每收到 N 次请求就重启一次编码器强制出 I 帧。重启编码器有 100~200ms 的黑屏所以只作为兜底。验证这两个机制是否生效我习惯用两个指标一是接收端缓冲队列深度稳定在 3~5 帧算正常持续增长说明码率偏高二是花屏恢复时间从请求 I 帧到画面正常理想情况 500ms 以内。如果超过 1 秒检查一下网络 RTT 和编码器关键帧间隔是不是设太大了。从那以后我每次调投屏参数都强制先跑一遍弱网模拟用限速工具把带宽压到 2Mbps、丢包 5%观察码率自适应能不能把画面稳住。这个习惯帮我省了很多「实验室正常、现场翻车」的后悔药。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Python处理Excel实战:openpyxl从入门到进阶全攻略

Python处理Excel实战:openpyxl从入门到进阶全攻略

写Python的人,几乎没人能躲开Excel。不管你是做数据分析、自动化办公,还是写后端系统对接报表,Excel就像办公软件界的老大哥,永远坐在那张椅子上,等你把数据塞进去再拽出来。而openpyxl,就是我在处理.xlsx格…

2026/10/16 13:22:32 阅读更多 →
Redux combineReducers与bindActionCreators原理深度解析

Redux combineReducers与bindActionCreators原理深度解析

1. 这不是“读源码”,而是搞懂 Redux 的心脏协同机制你打开 Redux 官方文档,看到combineReducers和bindActionCreators这两个 API,第一反应可能是:“哦,一个用来合并 reducer,一个用来绑定 action creator”…

2026/10/16 13:22:32 阅读更多 →
M1 Mac SSD读写量查询与寿命解读:TBW、Swap与健康度排查指南

M1 Mac SSD读写量查询与寿命解读:TBW、Swap与健康度排查指南

1. 事情起因:为什么突然都在查SSD读写量最早注意到这件事,是某技术社区里一位网友发帖说自己的M1 MacBook Air用了不到一年,硬盘健康度掉得厉害。底下跟帖的人越来越多,有人晒出自己机器的读写数据,有人开始担心自己的…

2026/10/16 13:22:32 阅读更多 →

最新新闻

Mosaico全景解读:面向Physical AI的机器人数据平台,5大核心能力让你告别.bag文件

Mosaico全景解读:面向Physical AI的机器人数据平台,5大核心能力让你告别.bag文件

【免费下载链接】mosaico Mosaico - The data platform for Physical AI 项目地址: https://gitcode.com/gh_mirrors/mosai/mosaico 点击查看 免费下载 Mosaico 是一款面向 Physical AI 与机器人场景的开源数据平台,能把 ROS .bag、.mcap 这类单体传感器…

2026/10/16 13:52:36 阅读更多 →
云溪天工开料优化软件实操指南:提升板材利用率与余料管理

云溪天工开料优化软件实操指南:提升板材利用率与余料管理

简介:面向板式家具加工与定制生产场景,这套云溪天工开料优化软件免锁版专为衣柜、橱柜、鞋柜、酒架等板材排版而设计,支持雕刻机、电子锯、数控钻、侧孔机等多种切割设备。软件核心优势集中在超强算法、锯缝处理、原片修边、成品公差、余料回…

2026/10/16 13:52:36 阅读更多 →
C++ Win32游戏开发实战:从零实现捕鱼达人核心机制

C++ Win32游戏开发实战:从零实现捕鱼达人核心机制

简介:这是一份基于Windows平台的C课程设计项目,主题为捕鱼达人小游戏,适合正在学习Windows编程或需要完成类似课程设计的高校学生参考。项目展示了如何利用Windows API创建窗口、处理消息循环与鼠标输入,并结合GDI绘图绘制背景、鱼…

2026/10/16 13:52:36 阅读更多 →
阿里云专有云CSB V3.7.1运维实战:进程端口日志四层定位法

阿里云专有云CSB V3.7.1运维实战:进程端口日志四层定位法

简介:本资源是阿里云专有云企业版V3.7.1中云服务总线(CSB)的官方运维指南PDF文档,面向专有云环境下的系统运维工程师、中间件管理员及企业级云平台维护人员,聚焦CSB服务的部署、配置、监控与故障排查等核心运维场景。文…

2026/10/16 13:52:36 阅读更多 →
MATLAB遗传算法ga函数参数调优实战指南

MATLAB遗传算法ga函数参数调优实战指南

简介:本资源是面向MATLAB初学者与优化算法实践者的遗传算法工具箱实战包,聚焦非线性、多目标及复杂约束下的工程优化问题求解,特别适配MATLAB 2017a环境。压缩包含65个文件(214KB),主体为60个.m函数文件&am…

2026/10/16 13:52:36 阅读更多 →
C#.NET整合微信支付宝银联支付:回调验签与统一网关实践

C#.NET整合微信支付宝银联支付:回调验签与统一网关实践

简介:面向C# .NET开发人员的多支付渠道整合方案,聚焦微信、支付宝与银联支付在统一业务后台中的接入与协同。内容覆盖微信JSAPI、Native、H5、APP,支付宝网页支付、APP支付、扫码支付,以及银联网上支付、快捷支付等典型场景&#…

2026/10/16 13:51:34 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →