前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何实现Android端获取RTSP或RTMP流转推RTMP

如何实现Android端获取RTSP或RTMP流转推RTMP

原创
作者头像
音视频牛哥
修改于 2021-04-07 06:44:46
修改于 2021-04-07 06:44:46
3.2K00
代码可运行
举报
运行总次数:0
代码可运行

技术背景

最近不少开发者找到我们,他们在做智能家居等传统行业时,希望实现在Android板件拉取本地的RTSP或RTMP流,然后对外推送RTMP出去,亦或内部启个轻量级RTSP服务,提供个对外对接的媒介URL,简单来说,设计架构图如下:

基于上诉诉求,我们以大牛直播SDK官方)Android端的 SmartRelayDemoV2 工程为例,大概介绍下相关实现。

整体设计

1. 拉流:通过RTSP|RTMP直播播放SDK的数据回调接口,拿到音视频数据;

2. 转推:通过RTMP直播推送SDK的编码后数据输入接口,把回调上来的数据,传给RTMP直播推送模块,实现RTSP|RTMP数据流到RTMP服务器的转发;

3. 录像:如果需要录像,借助RTSP|RTMP直播播放SDK,拉到音视频数据后,直接存储MP4文件即可;

4. 快照:如果需要实时快照,拉流后,解码调用播放端快照接口,生成快照,因为快照涉及到video数据解码,如无必要,可不必开启,不然会额外消耗性能。

5. 拉流预览:如需预览拉流数据,只要调用播放端的播放接口,即可实现拉流数据预览;

6. 数据转AAC后转发:考虑到好多监控设备出来的音频可能是PCMA/PCMU的,如需要更通用的音频格式,可以转AAC后,在通过RTMP推送;

7. 转推RTMP实时静音:只需要在传audio数据的地方,加个判断即可;

8. 拉流速度反馈:通过RTSP播放端的实时码率反馈event,拿到实时带宽占用即可;

9. 整体网络状态反馈:考虑到有些摄像头可能会临时或异常关闭,RTMP服务器亦是,可以通过推拉流的event回调状态,查看那整体网络情况,如此界定:是拉不到流,还是推不到RTMP服务器;

10. 数据注入轻量级RTSP服务:拉流的数据,注入轻量级RTSP服务,对外提供RTSP URL。

先上图

Demo主要实现了以下几个功能点展示:

1. 设置RTMP、RTSP拉流的URL;

2. 设置转推RTMP的URL;

3. 实时播放|录像过程中,实时静音、实施快照;

4. 实时播放;

5. 实时录像;

6. 拉取的流数据,实时转推,对应“开始推流”;

7. 拉取的流数据,注入轻量级RTSP服务,启动服务后,发布RTSP流,对外提供可访问的RTSP URL。

注意:以上播放、录像、转推RTMP、注入轻量级RTSP服务四者是可单独工作,也可随时启动或停止相关功能,互不影响。

相关代码实现

开始拉流

拉流的目的,主要是启动数据回调,注意:拉流并不是直接播放出来窗口,只是拿数据,如果需要本地预览拉流数据,可以点击“开始播放”。

注意:“开始推流”和“发布RTSP流”之前,一定要先“开始拉流”,拿到音视频数据。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean StartPull()
	{
		if ( isPulling )
			return false;

		if (!OpenPullHandle())
			return false;

		libPlayer.SmartPlayerSetAudioDataCallback(playerHandle, new PlayerAudioDataCallback());
		libPlayer.SmartPlayerSetVideoDataCallback(playerHandle, new PlayerVideoDataCallback());

		int is_pull_trans_code  = 1;
		libPlayer.SmartPlayerSetPullStreamAudioTranscodeAAC(playerHandle, is_pull_trans_code);

		int startRet = libPlayer.SmartPlayerStartPullStream(playerHandle);

		if (startRet != 0) {
			Log.e(TAG, "Failed to start pull stream!");

			if(!isPlaying && !isRecording && isPushing && !isRTSPPublisherRunning)
			{
				libPlayer.SmartPlayerClose(playerHandle);
				playerHandle = 0;
			}

			return false;
		}

		isPulling = true;
		return true;
	}

这里调到OpenPullHandle()封装,其实就是启动调研Player的Open()接口,获取到player handle,然后设置一下基础数据接口,比如event callback,buffer time,TCP/UDP模式、拉流的URL等;

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean OpenPullHandle()
	{
		if (playerHandle != 0) {
			return true;
		}

		playbackUrl = "rtsp://admin:daniulive12345@192.168.0.120:554/h265/ch1/main/av_stream";

		if (playbackUrl == null) {
			Log.e(TAG, "playback URL with NULL...");
			return false;
		}

		playerHandle = libPlayer.SmartPlayerOpen(myContext);

		if (playerHandle == 0) {
			Log.e(TAG, "playerHandle is nil..");
			return false;
		}

		libPlayer.SetSmartPlayerEventCallbackV2(playerHandle,
				new EventHandePlayerV2());

		libPlayer.SmartPlayerSetBuffer(playerHandle, playBuffer);

		// set report download speed
		// libPlayer.SmartPlayerSetReportDownloadSpeed(playerHandle, 1, 5);

		//设置RTSP超时时间
		int rtsp_timeout = 12;
		libPlayer.SmartPlayerSetRTSPTimeout(playerHandle, rtsp_timeout);

		//设置RTSP TCP/UDP模式自动切换
		int is_auto_switch_tcp_udp = 1;
		libPlayer.SmartPlayerSetRTSPAutoSwitchTcpUdp(playerHandle, is_auto_switch_tcp_udp);

		libPlayer.SmartPlayerSaveImageFlag(playerHandle, 1);

		// It only used when playback RTSP stream..
		//libPlayer.SmartPlayerSetRTSPTcpMode(playerHandle, 1);

		libPlayer.SmartPlayerSetUrl(playerHandle, playbackUrl);

		return true;
	}

停止拉流

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private void StopPull()
	{
		if ( !isPulling )
			return;

		libPlayer.SmartPlayerStopPullStream(playerHandle);

		if ( !isPlaying && !isRecording && !isPushing && !isRTSPPublisherRunning)
		{
			libPlayer.SmartPlayerClose(playerHandle);
			playerHandle = 0;
		}

		isPulling = false;
	}

开始播放

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean StartPlay()
	{
		if (!OpenPullHandle())
			return false;

		// 如果第二个参数设置为null,则播放纯音频
		libPlayer.SmartPlayerSetSurface(playerHandle, sSurfaceView);

		libPlayer.SmartPlayerSetRenderScaleMode(playerHandle, 1);

		// External Render test
		// libPlayer.SmartPlayerSetExternalRender(playerHandle, new
		// RGBAExternalRender());
		// libPlayer.SmartPlayerSetExternalRender(playerHandle, new
		// I420ExternalRender());

		libPlayer.SmartPlayerSetFastStartup(playerHandle, isFastStartup ? 1 : 0);

		libPlayer.SmartPlayerSetAudioOutputType(playerHandle, 1);

		if (isMute) {
			libPlayer.SmartPlayerSetMute(playerHandle, isMute ? 1
					: 0);
		}

		if (isHardwareDecoder)
		{
			int isSupportHevcHwDecoder = libPlayer.SetSmartPlayerVideoHevcHWDecoder(playerHandle, 1);

			int isSupportH264HwDecoder = libPlayer
					.SetSmartPlayerVideoHWDecoder(playerHandle, 1);

			Log.i(TAG, "isSupportH264HwDecoder: " + isSupportH264HwDecoder + ", isSupportHevcHwDecoder: " + isSupportHevcHwDecoder);
		}

		libPlayer.SmartPlayerSetLowLatencyMode(playerHandle, isLowLatency ? 1
				: 0);

		libPlayer.SmartPlayerSetRotation(playerHandle, rotate_degrees);

		int iPlaybackRet = libPlayer
				.SmartPlayerStartPlay(playerHandle);

		if (iPlaybackRet != 0) {
			Log.e(TAG, "StartPlay failed!");

			if ( !isPulling && !isRecording && !isPushing && !isRTSPPublisherRunning)
			{
				libPlayer.SmartPlayerClose(playerHandle);
				playerHandle = 0;
			}

			return false;
		}

		isPlaying = true;
		return true;
	}

停止播放

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private void StopPlay()
	{
		if ( !isPlaying )
			return;

		isPlaying = false;

		libPlayer.SmartPlayerStopPlay(playerHandle);

		if ( !isPulling && !isRecording && !isPushing && !isRTSPPublisherRunning)
		{
			libPlayer.SmartPlayerClose(playerHandle);
			playerHandle = 0;
		}
	}

开始录像

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean StartRecorder()
	{
		if (!OpenPullHandle())
			return false;

		ConfigRecorderFuntion();

		int iRecRet = libPlayer
				.SmartPlayerStartRecorder(playerHandle);

		if (iRecRet != 0) {
			Log.e(TAG, "StartRecorder failed!");

			if ( !isPulling &&!isPlaying && !isPushing && !isRTSPPublisherRunning)
			{
				libPlayer.SmartPlayerClose(playerHandle);
				playerHandle = 0;
			}

			return false;
		}

		isRecording = true;
		return true;
	}

停止录像

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private void StopRecorder()
	{
		if ( !isRecording )
			return;

		isRecording = false;

		libPlayer.SmartPlayerStopRecorder(playerHandle);

		if ( !isPlaying && !isPulling && !isPushing && !isRTSPPublisherRunning)
		{
			libPlayer.SmartPlayerClose(playerHandle);
			playerHandle = 0;
		}
	}

开始推流

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean StartPush()
	{
		if (isPushing)
			return false;

		relayStreamUrl = "rtmp://192.168.0.211:1935/hls/stream1";

		if (relayStreamUrl == null) {
			Log.e(TAG, "StartPush URL is null...");
			return false;
		}

		if (!OpenPushHandle())
			return false;

		if ( libPublisher.SmartPublisherSetURL(publisherHandle, relayStreamUrl) != 0 )
		{
			Log.e(TAG, "StartPush failed!");
		}

		int startRet = libPublisher.SmartPublisherStartPublisher(publisherHandle);
		if( startRet != 0)
		{
			Log.e(TAG, "Failed to call StartPublisher!");

			if(isRTSPPublisherRunning)
			{
				libPublisher.SmartPublisherClose(publisherHandle);
				publisherHandle = 0;
			}

			return false;
		}

		isPushing = true;

		return true;
	}

开始推流调到了OpenPushHandle()封装,具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean OpenPushHandle()
	{
		if(publisherHandle != 0)
		{
			return true;
		}

		int audio_opt = 2;
		int video_opt = 2;

		int videoWidth = 640;
		int videoHeight  = 480;

		publisherHandle = libPublisher.SmartPublisherOpen(myContext, audio_opt, video_opt,
				videoWidth, videoHeight);

		if (publisherHandle == 0 )
		{
			Log.e(TAG, "OpenPushHandle failed!");
			return false;
		}

		Log.i(TAG, "publisherHandle=" + publisherHandle);

		libPublisher.SetSmartPublisherEventCallbackV2(publisherHandle, new EventHandePublisherV2());

		return true;
	}

停止推流

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	public void StopPush()
	{
		if (!isPushing)
			return;

		isPushing = false;

		libPublisher.SmartPublisherStopPublisher(publisherHandle);

		if(!isRTSPPublisherRunning && !isRTSPServiceRunning)
		{
			libPublisher.SmartPublisherClose(publisherHandle);
			publisherHandle = 0;
		}
	}

启动RTSP服务

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//启动/停止RTSP服务
	class ButtonRtspServiceListener implements OnClickListener {
		public void onClick(View v) {
			if (isRTSPServiceRunning) {
				stopRtspService();

				btnRtspService.setText("启动RTSP服务");
				btnRtspPublisher.setEnabled(false);

				isRTSPServiceRunning = false;
				return;
			}

			if(!OpenPushHandle())
			{
				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端口失败! 请检查端口是否重复或者端口不在范围内!");
				}

				//String user_name = "admin";
				//String password = "12345";
				//libPublisher.SetRtspServerUserNamePassword(rtsp_handle_, user_name, password);

				//一般来说单播网络设备支持的好,wifi组播很多路由器不支持,默认单播模式;如需使用组播模式,确保设备支持后,打开注释代码测试即可
                /*
                boolean is_enable_multicast = true;

                if(is_enable_multicast)
                {
                    int is_multicast = 1;

                    libPublisher.SetRtspServerMulticast(rtsp_handle_, is_multicast);

                    boolean is_enable_ssm_multicast = true;

                    String multicast_address = "";

                    if(is_enable_ssm_multicast)
                    {
                        multicast_address = MakeSSMMulticastAddress();
                    }
                    else
                    {
                        multicast_address = MakeMulticastAddress();
                    }

                    Log.i(TAG, "is_enable_ssm_multicast:" + is_enable_ssm_multicast + " multiAddr: " + multicast_address);

                    libPublisher.SetRtspServerMulticastAddress(rtsp_handle_, multicast_address);
                }
                */

				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;
			}
		}
	}

停止RTSP服务

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//停止RTSP服务
	private void stopRtspService() {
		if(!isRTSPServiceRunning)
			return;

		if (libPublisher != null && rtsp_handle_ != 0) {
			libPublisher.StopRtspServer(rtsp_handle_);
			libPublisher.CloseRtspServer(rtsp_handle_);
			rtsp_handle_ = 0;
		}

		if(!isPushing)
		{
			libPublisher.SmartPublisherClose(publisherHandle);
			publisherHandle = 0;
		}

		isRTSPServiceRunning = false;
	}

开始发布RTSP流

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	private boolean StartRtspStream()
	{
		if (isRTSPPublisherRunning)
			return false;

		String rtsp_stream_name = "stream1";
		libPublisher.SetRtspStreamName(publisherHandle, rtsp_stream_name);
		libPublisher.ClearRtspStreamServer(publisherHandle);

		libPublisher.AddRtspStreamServer(publisherHandle, rtsp_handle_, 0);

		if (libPublisher.StartRtspStream(publisherHandle, 0) != 0)
		{
			Log.e(TAG, "调用发布rtsp流接口失败!");

			if (!isPushing)
			{
				libPublisher.SmartPublisherClose(publisherHandle);
				publisherHandle = 0;
			}

			return false;
		}

		isRTSPPublisherRunning = true;
		return true;
	}

停止发布RTSP流

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//停止发布RTSP流
	private void stopRtspPublisher()
	{
		if(!isRTSPPublisherRunning)
			return;

		if (libPublisher != null) {
			libPublisher.StopRtspStream(publisherHandle);
		}

		if (!isPushing && !isRTSPServiceRunning)
		{
			libPublisher.SmartPublisherClose(publisherHandle);
			publisherHandle = 0;
		}

		isRTSPPublisherRunning = false;
	}

获取RTSP连接会话数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	//当前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();
	}

	//获取RTSP会话数
	class ButtonGetRtspSessionNumbersListener implements 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);
			}
		}
	};

总结

以上是大概的流程,感兴趣的开发者可自行参考。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
Power Query 真经 - 第 4 章 - 在 Excel 和 Power BI 之间迁移查询
Power Query 可以在 Power BI 或 Excel 中使用,很多人一开始就在想到底用哪个平台来使用 Power Query,其实不必为此纠结,总有一天会意识到需要把查询复制到一个另一个中的。这有可能是将查询从一个 Excel 工作簿中复制到另一个 Excel 工作簿中,从 Excel 复制到 Power BI,或者从 Power BI 复制到 Excel。在本章中,将探讨将查询从一个工具快速移植到另一个工具的方法。请记住,虽然本书的重点是 Excel 和 Power BI,但这些步骤对于任何承载 Power Query 的工具来说几乎是相同的,即使它包含在其他微软产品或服务中。
BI佐罗
2022/04/02
8.3K0
Power Query 真经 - 第 4 章 - 在 Excel 和 Power BI 之间迁移查询
Power Query 真经 - 第 6 章 - 从Excel导入数据
毫无疑问,对于开始就以表格形式处理数据的人来说,最简单的方法之一是打开 Excel 并开始在工作表中记录数据。虽然 Excel 并不是真正打算充当数据库的角色,但这正是实际发生的事情,因此 Power Query 将 Excel 文件和数据视为有效数据源。
BI佐罗
2022/05/17
17.2K1
Power Query 真经 - 第 6 章 - 从Excel导入数据
Power Query 真经 - 第 8 章 - 纵向追加数据
数据专业人员经常做的工作之一是将多个数据集追加到一起。无论这些数据集是包含在一个 Excel 工作簿中,还是分布在多个文件中,问题是它们需要被纵向【追加】到一个表中。
BI佐罗
2022/05/17
7.4K0
Power Query 真经 - 第 8 章 - 纵向追加数据
ChatGPT Excel 大师
欢迎来到 Excel 掌握的变革之旅,在这里,尖端技术和永恒专业知识在“ChatGPT Excel 掌握:释放专家技巧和窍门的力量”中融合。在当今快节奏的环境中,Excel 仍然是各行业专业人士的必备工具,而借助 ChatGPT 的融入,其潜力已经超出想象。
ApacheCN_飞龙
2024/05/24
7910
PQ语言规范
Microsoft Power Query 提供了强大的“获取数据”体验,其中包含许多功能。Power Query 的一项核心功能是筛选和组合,即“混搭”来自一个或多个受支持数据源的丰富集合中的数据。任何此类数据混搭均使用 Power Query 公式语言(非正式称为“M”)表示。Power Query 将 M 文档嵌入 Excel 和 Power BI 工作簿中,以实现可重复的数据混搭。
冬夜先生
2022/01/04
1K0
轻松搞定复杂表单数据,快速提升办公数字化能力
“IT有得聊”是机械工业出版社旗下IT专业资讯和服务平台,致力于帮助读者在广义的IT领域里,掌握更专业、更实用的知识与技能,快速提升职场竞争力。 点击蓝色微信名可快速关注我们。
大海Power
2023/09/09
4460
轻松搞定复杂表单数据,快速提升办公数字化能力
Power Query 真经 - 第 3 章 - 数据类型与错误
本章专门讨论 Power Query 新手会面临的两个常见问题:理解 Power Query 是基于数据类型(而不是数据格式)的工具,以及如何理解和处理 Power Query 查询中的错误。
BI佐罗
2022/04/02
6.2K0
Power Query 真经 - 第 3 章 - 数据类型与错误
DeepSeek系列:90%的人不知道的DeepSeek+Excel联用秘籍
在数字化办公时代,Excel数据处理效率直接决定职场人的竞争力。DeepSeek作为AI办公神器,能通过精准提示词生成复杂公式、自动化清洗数据、跨表统计等操作,将繁琐流程压缩至秒级完成。本文结合企业实战场景,整理25个可直接复用的提示词案例,助你成为Excel效率王者。
小明互联网技术分享社区
2025/05/02
3940
DeepSeek系列:90%的人不知道的DeepSeek+Excel联用秘籍
Power Query 真经 - 第 7 章 - 常用数据转换
分析师面临的普遍问题是,无论从哪里获得数据,大部分情况都是一种不能立即使用的状态。因此,不仅需要时间把数据加载到文件中,还得花更多的时间来清洗它,改变它的结构,以便后续做分析的时候能更好的使用这个数据。
BI佐罗
2022/05/17
8.1K0
Power Query 真经 - 第 7 章 - 常用数据转换
Upspin 中的错误处理
Upspin 项目使用自定义的包 —— upspin.io/errors —— 来表示系统内部出现的错误条件。这些错误满足标准的 Go error 接口,但是使用的是自定义类型 upspin.io/errors.Error,该类型具有一些已经证明对项目有用的属性。 这里,我们会演示这个包是如何工作的,以及如何使用这个包。这个故事为关于 Go 中的错误处理更广泛的讨论提供了经验教训。 动机 在项目进行几个月后,我们清楚地知道,我们需要一致的方法来处理整个代码中的错误构建、描述和处理。我们决定实现一个自定义
企鹅号小编
2018/02/07
2.4K0
Upspin 中的错误处理
Power Query批量导入Excel文件,和导入文本有一点儿不同
小勤:大海,你上次说PowerQuery可以批量导入Excel文件,我参考你那个批量导入文本文件的方法试了一下,不行啊。
大海Power
2021/08/30
2K0
Excel实战技巧:如何使用Excel数据表创建蒙特卡罗模型和预测
引言:本文学习整理自exceluser.com,非常好的一篇文章,特分享于此,供有兴趣的朋友参考。
fanjy
2023/02/14
5.3K0
Excel实战技巧:如何使用Excel数据表创建蒙特卡罗模型和预测
Power Query 真经 - 第 5 章 - 从平面文件导入数据
作为一名数据专家,日常工作很可能都是在使用数据之前对其进行导入、操作和转换。可悲的是,许多人都没有机会接触到拥有精心策划过的数据的大数据库。相反,被不断地喂食 “TXT” 或 “CSV” 文件,并且在开始分析之前,必须经历将它们导入到 Excel 或 Power BI 解决方案的过程。对用户来说,重要的商业信息往往是以以下格式存储或发送给用户的。
BI佐罗
2022/05/17
5.7K0
Power Query 真经 - 第 5 章 - 从平面文件导入数据
《Python for Excel》读书笔记连载1:为什么为Excel选择Python?
本节为《Chapter 1:Why Python for Excel?》的第一部分,简单地讲解了Excel的历史,Excel编程的最佳实践,以及Excel为适应发展而作出的变化。 当你每天花费很多时间
fanjy
2021/11/26
5.9K0
表格控件:计算引擎、报表、集算表
近日,葡萄城正式发布了SpreadJS最新版本 V17.1,为前端表格控件市场带来了一系列令人瞩目的新特性和功能增强。本次更新旨在进一步提升用户在计算引擎、报表生成和分析等方面的体验,为各行业的开发者提供更强大的工具支持。
葡萄城控件
2024/08/22
9950
表格控件:计算引擎、报表、集算表
Power BI:关于FilterDatabase和命名区域(DefinedName)
文章背景:在使用文件夹的方式批量导入多份Excel文件时,由于Excel文件中存在隐藏的筛选行,所以导致数据重复录入,后来看了赵文超老师的一篇文章(见文末的参考资料1),对这一问题有了新的认识。
Exploring
2023/08/17
3K0
Power BI:关于FilterDatabase和命名区域(DefinedName)
Excelize 发布 2.7.1 版本,Go 语言 Excel 文档基础库
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Excel、WPS、OpenOffice 等电子表格办公应用创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。入选 2020 Gopher China - Go 领域明星开源项目(GSP)、2018 年开源中国码云最有价值开源项目 GVP (Gitee Most Valuable Project),2022 中国开源创新大赛优秀项目。
xuri
2023/04/11
1.8K0
Excelize 发布 2.7.1 版本,Go 语言 Excel 文档基础库
excel常用操作大全
例如,在excel中输入单位的人员信息后,如果需要在原出生年份的数字前再加两位数字,即在每个人的出生年份前再加两位数字19,如果逐个修改太麻烦,那么我们可以使用以下方法来节省时间和精力:
崩天的勾玉
2021/12/20
21.5K0
excel常用操作大全
Extreme DAX-第3章 DAX 的用法
Power BI 模型的真正强大之处在于通过使用 DAX 语言进行计算。虽然许多 Power BI 用户专注于模型并试着完全避开使用 DAX,但是除了最简单的基础聚合运算以外,其他所有的计算都需要通过 DAX 来实现。而且,你迟早会在 Power BI 中遇到更复杂的计算需求。根据我们的经验,典型的情况会是:你精心制作的一个 Power BI 报告初稿,会引出有关这些数据的越来越多、越来越复杂的问题。
陈学谦
2022/05/24
7.7K0
Extreme DAX-第3章 DAX 的用法
使用Power Query处理数据(二)
在电商行业的数据管理中,一般会把库存数据转换成单条数据的数据表,在传统Excel中操作比较耗时耗力。而使用Power Query,就会变得十分轻松。假如我们需要将图1的数据转换成图2的数据样式。
数据山谷
2020/12/22
1.1K0
推荐阅读
相关推荐
Power Query 真经 - 第 4 章 - 在 Excel 和 Power BI 之间迁移查询
更多 >
LV.5
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档