如何在Android上实现RTSP服务器
技术背景
在Android上实现RTSP服务器确实是一个不太常见的需求,因为Android平台主要是为客户端应用设计的。在一些内网场景下,我们更希望把安卓终端或开发板,作为一个IPC(网络摄像机)一样,对外提供个拉流的rtsp url,然后把摄像头麦克风甚至屏幕采集的数据,共享出去,轻量级RTSP的设计理念脱颖而出。
轻量级RTSP服务设计初衷,就是避免用户单独部署RTSP或者RTMP服务,实现本地的音视频数据(如摄像头、麦克风),编码后,汇聚到内置RTSP服务,对外提供可供拉流的RTSP URL,轻量级RTSP服务,适用于内网环境下,对并发要求不高的场景,支持H.264/H.265,支持RTSP鉴权、单播、组播模式,考虑到单个服务承载能力,我们支持同时创建多个RTSP服务,并支持获取当前RTSP服务会话连接数。

如何实现RTSP服务器
如果你找不到合适的库,或者需要更高级的功能,你可以考虑编写自己的RTSP服务器:
- 了解RTSP协议:首先,你需要深入了解RTSP协议的工作原理,包括其消息格式、会话管理和流控制机制。
- 网络编程:在Android上,你可以使用Java的Socket API来处理网络通信,也可以直接底层实现,然后对上提供jni的接口。你需要编写代码来监听端口、接收RTSP请求、解析请求并发送响应。
- 媒体处理:RTSP服务器需要能够捕获、编码和传输媒体数据。你可以使用Android的Camera或Camera2的API来捕获视频,并使用FFmpeg或Android的MediaCodec API来实现音视频数据编码。
- 多线程和并发:RTSP服务器需要处理多个并发客户端连接。你可以使用Java的线程或并发API来管理这些连接。
功能设计
一个完整的RTSP服务,需要设计的功能如下:
- [视频格式]H.264/H.265(Android H.265硬编码);
- [音频格式]G.711 A律、AAC;
- 协议:RTSP;
- [音量调节]Android平台采集端支持实时音量调节;
- [H.264硬编码]支持H.264特定机型硬编码;
- [H.265硬编码]支持H.265特定机型硬编码;
- [音视频]支持纯音频/纯视频/音视频;
- [摄像头]支持采集过程中,前后摄像头实时切换;
- 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
- [实时水印]支持动态文字水印、png水印;
- [实时快照]支持实时快照;
- [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
- [外部编码前视频数据对接]支持YUV数据对接;
- [外部编码前音频数据对接]支持PCM对接;
- [外部编码后视频数据对接]支持外部H.264、H.265数据对接;
- [外部编码后音频数据对接]外部AAC数据对接;
- [扩展录像功能]支持和录像SDK组合使用,录像相关功能。
- 支持RTSP端口设置;
- 支持RTSP鉴权用户名、密码设置;
- 支持获取当前RTSP服务会话连接数;
- 支持Android 5.1及以上版本。
接口设计
| Android内置轻量级RTSP服务SDK接口详解 | ||
| 调用描述 | 接口 | 接口描述 |
| SmartRTSPServerSDK | ||
| 初始化RTSP Server | InitRtspServer | Init rtsp server(和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用) |
| 创建一个rtsp server | OpenRtspServer | 创建一个rtsp server,返回rtsp server句柄 |
| 设置端口 | SetRtspServerPort | 设置rtsp server 监听端口, 在StartRtspServer之前必须要设置端口 |
| 设置鉴权用户名、密码 | SetRtspServerUserNamePassword | 设置rtsp server 鉴权用户名和密码, 这个可以不设置,只有需要鉴权的再设置 |
| 获取rtsp server当前会话数 | GetRtspServerClientSessionNumbers | 获取rtsp server当前的客户会话数, 这个接口必须在StartRtspServer之后再调用 |
| 启动rtsp server | StartRtspServer | 启动rtsp server |
| 停止rtsp server | StopRtspServer | 停止rtsp server |
| 关闭rtsp server | CloseRtspServer | 关闭rtsp server |
| UnInit rtsp server | UnInitRtspServer | UnInit rtsp server(和InitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次UnInitRtspServer) |
| SmartRTSPServerSDK供Publisher调用的接口 | ||
| 设置rtsp的流名称 | SetRtspStreamName | 设置rtsp的流名称 |
| 给要发布的rtsp流设置rtsp server | AddRtspStreamServer | 给要发布的rtsp流设置rtsp server, 一个流可以发布到多个rtsp server上,rtsp server的创建启动请参考OpenRtspServer和StartRtspServer接口 |
| 清除设置的rtsp server | ClearRtspStreamServer | 清除设置的rtsp server |
| 启动rtsp流 | StartRtspStream | 启动rtsp流 |
| 停止rtsp流 | StopRtspStream | 停止rtsp流 |
调用逻辑
以大牛直播SDK的Android平台Camera2对接为例,先初始化RTSP Server,启动RTSP服务,然后发布RTSP流即可,如果需要停止RTSP服务,那么先停止RTSP流,再停止RTSP服务即可:

/** MainActivity.java* Author: daniusdk.com* WeChat: xinsheng120*/
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);...context_ = this.getApplicationContext();libPublisher = new SmartPublisherJniV2();libPublisher.InitRtspServer(context_); //和UnInitRtspServer配对使用,即便是启动多个RTSP服务,也只需调用一次InitRtspServer,请确保在OpenRtspServer之前调用
}
启动、停止RTSP服务:
//启动/停止RTSP服务
class ButtonRtspServiceListener implements View.OnClickListener {public void onClick(View v) {if (isRTSPServiceRunning) {stopRtspService();btnRtspService.setText("启动RTSP服务");btnRtspPublisher.setEnabled(false);isRTSPServiceRunning = false;return;}Log.i(TAG, "onClick start rtsp service..");rtsp_handle_ = libPublisher.OpenRtspServer(0);if (rtsp_handle_ == 0) {Log.e(TAG, "创建rtsp server实例失败! 请检查SDK有效性");} else {int port = 8554;if (libPublisher.SetRtspServerPort(rtsp_handle_, port) != 0) {libPublisher.CloseRtspServer(rtsp_handle_);rtsp_handle_ = 0;Log.e(TAG, "创建rtsp server端口失败! 请检查端口是否重复或者端口不在范围内!");}if (libPublisher.StartRtspServer(rtsp_handle_, 0) == 0) {Log.i(TAG, "启动rtsp server 成功!");} else {libPublisher.CloseRtspServer(rtsp_handle_);rtsp_handle_ = 0;Log.e(TAG, "启动rtsp server失败! 请检查设置的端口是否被占用!");}btnRtspService.setText("停止RTSP服务");btnRtspPublisher.setEnabled(true);isRTSPServiceRunning = true;}}
}
stopRtspService()实现如下:
//停止RTSP服务
private void stopRtspService() {if(!isRTSPServiceRunning){return;}if (libPublisher != null && rtsp_handle_ != 0) {libPublisher.StopRtspServer(rtsp_handle_);libPublisher.CloseRtspServer(rtsp_handle_);rtsp_handle_ = 0;}
}
发布、停止RTSP流:
//发布/停止RTSP流
class ButtonRtspPublisherListener implements View.OnClickListener {public void onClick(View v) {if (stream_publisher_.is_rtsp_publishing()) {stopRtspPublisher();btnRtspPublisher.setText("发布RTSP流");btnGetRtspSessionNumbers.setEnabled(false);btnRtspService.setEnabled(true);return;}Log.i(TAG, "onClick start rtsp publisher..");InitAndSetConfig();String rtsp_stream_name = "stream1";stream_publisher_.SetRtspStreamName(rtsp_stream_name);stream_publisher_.ClearRtspStreamServer();stream_publisher_.AddRtspStreamServer(rtsp_handle_);if (!stream_publisher_.StartRtspStream()) {stream_publisher_.try_release();Log.e(TAG, "调用发布rtsp流接口失败!");return;}startAudioRecorder();startLayerPostThread();btnRtspPublisher.setText("停止RTSP流");btnGetRtspSessionNumbers.setEnabled(true);btnRtspService.setEnabled(false);}
}
stopRtspPublisher()实现如下:
//停止发布RTSP流
private void stopRtspPublisher() {stream_publisher_.StopRtspStream();stream_publisher_.try_release();if (!stream_publisher_.is_publishing())stopAudioRecorder();
}
其中,InitAndSetConfig()实现如下,通过调研SmartPublisherOpen()接口,生成推送实例句柄。
/** MainActivity.java* Author: daniusdk.com*/
private void InitAndSetConfig() {if (null == libPublisher)return;if (!stream_publisher_.empty())return;Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);int audio_opt = 1;long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3, video_width_, video_height_);if (0==handle) {Log.e(TAG, "sdk open failed!");return;}Log.i(TAG, "publisherHandle=" + handle);int fps = 25;int gop = fps * 3;initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);stream_publisher_.set(libPublisher, handle);
}
对应的initialize_publisher()实现如下,设置软硬编码、帧率、关键帧间隔等。
private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {if (null == lib_publisher) {Log.e(TAG, "initialize_publisher lib_publisher is null");return false;}if (0 == handle) {Log.e(TAG, "initialize_publisher handle is 0");return false;}if (videoEncodeType == 1) {int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);Log.i(TAG, "h264HWKbps: " + kbps);int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);if (isSupportH264HWEncoder == 0) {lib_publisher.SetNativeMediaNDK(handle, 0);lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBRlib_publisher.SetVideoHWEncoderQuality(handle, 39);lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High// lib_publisher.SetAVCHWEncoderLevel(handle, 0x200); // Level 3.1// lib_publisher.SetAVCHWEncoderLevel(handle, 0x400); // Level 3.2// lib_publisher.SetAVCHWEncoderLevel(handle, 0x800); // Level 4lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了//lib_publisher.SetAVCHWEncoderLevel(handle, 0x2000); // Level 4.2// lib_publisher.SetVideoHWEncoderMaxBitrate(handle, ((long)h264HWKbps)*1300);Log.i(TAG, "Great, it supports h.264 hardware encoder!");}} else if (videoEncodeType == 2) {int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);Log.i(TAG, "hevcHWKbps: " + kbps);int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);if (isSupportHevcHWEncoder == 0) {lib_publisher.SetNativeMediaNDK(handle, 0);lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBRlib_publisher.SetVideoHWEncoderQuality(handle, 39);// libPublisher.SetVideoHWEncoderMaxBitrate(handle, ((long)hevcHWKbps)*1200);Log.i(TAG, "Great, it supports hevc hardware encoder!");}}boolean is_sw_vbr_mode = true;//H.264 software encoderif (is_sw_vbr_mode) {int is_enable_vbr = 1;int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);}if (is_pcma_) {lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);} else {lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);}lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);lib_publisher.SmartPublisherSetGopInterval(handle, gop);lib_publisher.SmartPublisherSetFPS(handle, fps);// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);boolean is_noise_suppression = true;lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);boolean is_agc = false;lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);int echo_cancel_delay = 0;lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);return true;
}
发布RTSP流成功后,会回调上来可供拉流的RTSP URL:
private static class EventHandlerPublisherV2 implements NTSmartEventCallbackV2 {@Overridepublic void onNTSmartEventCallbackV2(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {switch (id) {...case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:publisher_event = "RTSP服务URL: " + param3;break;}}
}
获取RTSP Session会话数:
//获取RTSP会话数
class ButtonGetRtspSessionNumbersListener implements View.OnClickListener {public void onClick(View v) {if (libPublisher != null && rtsp_handle_ != 0) {int session_numbers = libPublisher.GetRtspServerClientSessionNumbers(rtsp_handle_);Log.i(TAG, "GetRtspSessionNumbers: " + session_numbers);PopRtspSessionNumberDialog(session_numbers);}}
}//当前RTSP会话数弹出框
private void PopRtspSessionNumberDialog(int session_numbers) {final EditText inputUrlTxt = new EditText(this);inputUrlTxt.setFocusable(true);inputUrlTxt.setEnabled(false);String session_numbers_tag = "RTSP服务当前客户会话数: " + session_numbers;inputUrlTxt.setText(session_numbers_tag);AlertDialog.Builder builderUrl = new AlertDialog.Builder(this);builderUrl.setTitle("内置RTSP服务").setView(inputUrlTxt).setNegativeButton("确定", null);builderUrl.show();
}
onDestroy() 的时候,调研UnInitRtspServer()即可:
@Override
protected void onDestroy() {Log.i(TAG, "activity destory!");stopAudioRecorder();stopRtspPublisher();stopRtspService();isRTSPServiceRunning = false;stream_publisher_.release();if (libPublisher != null)libPublisher.UnInitRtspServer(); //如已启用内置服务功能(InitRtspServer),调用UnInitRtspServer, 注意,即便是启动多个RTSP服务,也只需调用UnInitRtspServer一次stopLayerPostThread();if (camera2Helper != null) {camera2Helper.release();}super.onDestroy();
}
总结
Android上实现RTSP服务器是一个极具挑战的任务,功能设计这块,除了需要支持接编码前音视频数据外,还需要支持对接编码后音视频数据,并实现本地录像、快照等功能组合使用。需要注意的是,就像海康、大华的摄像头一样,对外的并发,一般限于4-8个,Android设备的性能一般来说,可能不足以处理高负载的RTSP服务器,但是小并发模式下,能稳定的运行,就达到设计初衷了,感兴趣的开发者,可以单独跟单独探讨。
相关文章:
如何在Android上实现RTSP服务器
技术背景 在Android上实现RTSP服务器确实是一个不太常见的需求,因为Android平台主要是为客户端应用设计的。在一些内网场景下,我们更希望把安卓终端或开发板,作为一个IPC(网络摄像机)一样,对外提供个拉流的…...
代理导致的git错误
问题: 今天在clone时出现如下错误: fatal: unable to access https://github.com/NirDiamant/RAG_Techniques.git/: Failed to connect to 127.0.0.1 port 10089 after 2065 ms: Couldnt connect to server真是让人感到奇怪!就在前天&#…...
Ready Go
本文首发在这里 温馨提示 XX年,指的是20XX年,后跟以前、以后之类,均包含本数链接较多,只是想言之有物,已拒绝相同外链,仅看关心的即可已尽量只引用自己的东西,16年后仓库(11/13),2…...
Matlab simulink建模与仿真 第十三章(信号通路库)
参考视频:simulink1.1simulink简介_哔哩哔哩_bilibili 一、信号通路库中的模块概览 1、信号通路组 注:部分模块在第二章中有介绍,本章不再赘述。 2、信号存储和访问组 二、总线分配模块 Bus Assignment模块接受总线作为输入,并…...
Java中接口和抽象类的区别(语法层面的区别、设计理念层面的区别)
文章目录 1. 语法层面的区别1.1 成员属性1.2 成员方法1.3 关系 2. 设计理念层面的区别(重点)3. 举例理解抽象类和接口在设计理念层面的区别3.1 例一:门和警报3.2 例二:招聘3.3 例三:装修房子 4. 总结 1. 语法层面的区别…...
Leetcode面试经典150题-20.有效的括号
给定一个只包括 (,),{,},[,] 的字符串 s ,判断字符串是否有效。 有效字符串需满足: 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括…...
Git常用指令大全详解
Git常用指令大全详解 Git,作为目前最流行的分布式版本控制系统,其强大的功能和灵活性为开发者提供了极大的便利。无论是个人项目还是团队协作,Git都扮演着不可或缺的角色。本文将详细总结Git的常用指令,帮助大家更好地掌握这一工…...
面试真题-TCP的三次握手
TCP的基础知识 TCP头部 面试题:TCP的头部是多大? TCP(传输控制协议)的头部通常是固定的20个字节长,但是根据TCP选项(Options)的不同,这个长度可以扩展。TCP头部包含了许多关键的字…...
LabVIEW多语言支持优化
遇到的LabVIEW多语言支持问题,特别是德文显示乱码以及系统区域设置导致的异常,可能是由编码问题或区域设置不匹配引起的。以下是一些可能的原因及解决方案: 问题原因: 编码问题:LabVIEW内部使用UTF-8编码,但…...
身份证阅读器API模式 VUE Dorado7
VUE 新框架 // 身份证扫描 readIdCard(type) {// 1.连接axios.get(http://localhost:19196/openDevice).then(res > {if (res.data.resultFlag 0) {// 2.读卡axios.get(http://localhost:19196/readCard).then((res) > {if (res.data.resultFlag 0) {// this.$message…...
北京通州自闭症学校推荐:打造和谐学习氛围,助力孩子成长
在北京通州,寻找一所能够全面关注自闭症儿童成长、提供高效康复服务的学校,星贝育园无疑是众多家庭的首选。作为全国知名的广泛性发育障碍全托寄宿制儿童康复训练机构,星贝育园以其专业的康复方法、强大的师资力量和贴心的服务,为…...
openstack之cinder介绍
概念 cinder 为虚拟机提供管理块存储服务。支持的文件系统:lvm、iscsi、nfs、san、RBD 组件构成及功能介绍 cinder api:在控制节点运行,管理服务的接口,被命令行、其他组件调用; cinder scheduler:类似n…...
第k个排列 - 华为OD统一考试(E卷)
2024华为OD机试(E卷D卷C卷)最新题库【超值优惠】Java/Python/C合集 题目描述 给定参数n,从1到n会有n个整数:1,2,3,.,n,这n个数字共有 n!种排列。按大小顺序升序列出所有排列情况,并-一标记,当n3时,所有排列…...
清理C盘缓存,电脑缓存清理怎么一键删除,操作简单的教程
清理C盘缓存是维护电脑性能、释放磁盘空间的重要步骤。以下是一个详细且操作简单的教程,旨在帮助用户通过一键或几步操作完成C盘缓存的清理。 1.使用Windows系统自带工具 磁盘清理 1.打开磁盘清理工具: -按下“WinE”打开文件资源管理器…...
网络安全-ssrf
目录 一、环境 二、漏洞讲解 三、靶场讲解 四、可利用协议 4.1 dict协议 4.2 file协议 4.3 gopher协议 五、看一道ctf题吧(长亭的比赛) 5.1环境 5.2开始测试 编辑 一、环境 pikachu,这里我直接docker拉取的,我只写原…...
c++刷题
17.电话号码的组合 来源于题解思路: 继承 CC14 KiKi设计类继承 #include <iostream> #include <memory> using namespace std; class Shape{ private:int x;int y; };class Rectangle:public Shape { public:Rectangle(int length,int width):Shape…...
艾丽卡的区块链英语小课堂
系列文章目录 复习昨日 文章目录 系列文章目录前言1.opaque2.deduplicates3.references4,intermix5.serializing6.streamline7.robust8.flexibility9.exotic10.nevertheless11. realize12.flavor13.subtract14.attach15.award 前言 欢迎来到艾丽卡的区块链英语小课堂&#x…...
计算机毕业设计 公寓出租系统的设计与实现 Java实战项目 附源码+文档+视频讲解
博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…...
eclipse使用 笔记02
创建一个项目: 【File-->New-->Dynamic Web Project】 进入页面: Project name为项目命名 Target runtime:选择自己所对应的版本 finish创建成功: 创建成功后的删除操作: 创建前端界面: 【注意&a…...
基于C++实现(MFC)职工工作量统计系统
题目:职工工作量统计系统设计 1、问题描述 职工包括姓名、职工号、性别、年龄、所在部门、联系方式等信息。 工作量包括职工号、完成的产品数量等信息。 该设计系统能够对职工的工作量进行统计,并排出名次。注意,一个职工的工作量是可以多次…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
MySQL的pymysql操作
本章是MySQL的最后一章,MySQL到此完结,下一站Hadoop!!! 这章很简单,完整代码在最后,详细讲解之前python课程里面也有,感兴趣的可以往前找一下 一、查询操作 我们需要打开pycharm …...
保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...
数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
