obs录制功能源码分析

news2024/10/6 1:47:39

录制按钮

界面文件:

 

 主界面:OBSBasic.ui  中开始录制按钮的objectName 是   recordButton

槽函数:


void OBSBasic::on_recordButton_clicked()
{
    //1 输出模式是否被激活
	if (outputHandler->RecordingActive()) {
		bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow",
					       "WarnBeforeStoppingRecord");

		if (confirm && isVisible()) {
			QMessageBox::StandardButton button =
				OBSMessageBox::question(
					this, QTStr("ConfirmStopRecord.Title"),
					QTStr("ConfirmStopRecord.Text"),
					QMessageBox::Yes | QMessageBox::No,
					QMessageBox::No);

			if (button == QMessageBox::No) {
				ui->recordButton->setChecked(true);
				return;
			}
		}
		StopRecording();
	} else {
		if (!UIValidation::NoSourcesConfirmation(this)) {
			ui->recordButton->setChecked(false);
			return;
		}

        // 执行
		StartRecording();
	}
}

 调用堆栈:

 开始录制

void OBSBasic::StartRecording()
{
	if (outputHandler->RecordingActive())
		return;
	if (disableOutputsRef)
		return;

    // 1 检查磁盘是否有效
	if (!OutputPathValid()) {
		OutputPathInvalidMessage();
		ui->recordButton->setChecked(false);
		return;
	}
    
    //2 检查磁盘空间
	if (LowDiskSpace()) {
		DiskSpaceMessage();
		ui->recordButton->setChecked(false);
		return;
	}

	if (api)
		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTING);

	SaveProject();
   
    //3 开始录制 
	if (!outputHandler->StartRecording())
		ui->recordButton->setChecked(false);
}


// 简单输出
bool SimpleOutput::StartRecording()
{
	UpdateRecording();
	if (!ConfigureRecording(false))
		return false;
	if (!obs_output_start(fileOutput)) {
		QString error_reason;
		const char *error = obs_output_get_last_error(fileOutput);
		if (error)
			error_reason = QT_UTF8(error);
		else
			error_reason = QTStr("Output.StartFailedGeneric");
		QMessageBox::critical(main,
				      QTStr("Output.StartRecordingFailed"),
				      error_reason);
		return false;
	}

	return true;
}

调用libobs 

E:\opensrc\obs_src_19041\obs-studio-19141\obs-studio\libobs\obs-output.c

bool obs_output_start(obs_output_t *output)
{
	bool encoded;
	bool has_service;
	if (!obs_output_valid(output, "obs_output_start"))
		return false;
	if (!output->context.data)
		return false;

	has_service = (output->info.flags & OBS_OUTPUT_SERVICE) != 0;
	if (has_service && !obs_service_initialize(output->service, output))
		return false;

	encoded = (output->info.flags & OBS_OUTPUT_ENCODED) != 0;
	if (encoded && output->delay_sec) {
		return obs_output_delay_start(output);
	} else {
		if (obs_output_actual_start(output)) {
			do_output_signal(output, "starting");
			return true;
		}

		return false;
	}
}

停止录制:

void SimpleOutput::StopRecording(bool force)
{
	if (force)
		obs_output_force_stop(fileOutput);
	else
		obs_output_stop(fileOutput);
}

调用的都是libobs.dll的导出 导出方法   obs.h中声明

/** Starts the output. */
EXPORT bool obs_output_start(obs_output_t *output);

/** Stops the output. */
EXPORT void obs_output_stop(obs_output_t *output);

obs输出模式

高級输出

 

bool AdvancedOutput::StartRecording()
{
	const char *path;
	const char *recFormat;
	const char *filenameFormat;
	bool noSpace = false;
	bool overwriteIfExists = false;

	if (!useStreamEncoder) {
		if (!ffmpegOutput) {
			UpdateRecordingSettings();
		}
	} else if (!obs_output_active(streamOutput)) {
		UpdateStreamSettings();
	}

	UpdateAudioSettings();
    // 1 设置输出,ffmpeg 输出 or 录制输出
	if (!Active())
		SetupOutputs();

    //2 配置
	if (!ffmpegOutput || ffmpegRecording) {
		path = config_get_string(main->Config(), "AdvOut",
					 ffmpegRecording ? "FFFilePath"
							 : "RecFilePath");
		recFormat = config_get_string(main->Config(), "AdvOut",
					      ffmpegRecording ? "FFExtension"
							      : "RecFormat");
		filenameFormat = config_get_string(main->Config(), "Output",
						   "FilenameFormatting");
		overwriteIfExists = config_get_bool(main->Config(), "Output",
						    "OverwriteIfExists");
		noSpace = config_get_bool(main->Config(), "AdvOut",
					  ffmpegRecording
						  ? "FFFileNameWithoutSpace"
						  : "RecFileNameWithoutSpace");

		string strPath = GetRecordingFilename(path, recFormat, noSpace,
						      overwriteIfExists,
						      filenameFormat,
						      ffmpegRecording);

		obs_data_t *settings = obs_data_create();
		obs_data_set_string(settings, ffmpegRecording ? "url" : "path",
				    strPath.c_str());

		obs_output_update(fileOutput, settings);

		obs_data_release(settings);
	}

    //3 start
	if (!obs_output_start(fileOutput)) {
		QString error_reason;
		const char *error = obs_output_get_last_error(fileOutput);
		if (error)
			error_reason = QT_UTF8(error);
		else
			error_reason = QTStr("Output.StartFailedGeneric");
		QMessageBox::critical(main,
				      QTStr("Output.StartRecordingFailed"),
				      error_reason);
		return false;
	}

	return true;
}

录制

 

 为什么不使用ffmepeg命令行录制

  •  1 录制窗口鼠标闪烁
  • 2 无法处理屏幕分辨率非100%时的区域的录制不全
  • 3 热插拔时,无法更改采集源
  • 4 无法录制DirectUi窗口(无窗口句柄)

FFmpeg

Builds - CODEX FFMPEG @ gyan.dev

 链接:https://pan.baidu.com/s/1owwUbNEUzXQMARaOlfRH3w?pwd=yy3j 
提取码:yy3j

OBS采集模块

  • 显示器采集:core中的 libobs-d3d11和 libos-winrt  以及    duplicator-monitor-capture.c
  • 窗口采集:win-capture      window-capture.c
  •  游戏采集:game-capture.c
  • 采集卡采集:decklink
  • 摄像头采集:win-dshow

窗口采集方式

 

BitBlt的采集效率比较低,且只能采集一些有窗口句柄的窗口,无窗口句柄得用WGC。例如谷歌浏览器,VSCode 等都是无句柄窗口,这些窗口用BitBlt采集,显示都是黑色的

 

 E:\opensrc\obs_src_19041\obs-studio-19141\obs-studio\plugins\win-capture\window-capture.c

enum window_capture_method {
	METHOD_AUTO,
	METHOD_BITBLT,
	METHOD_WGC,
};

具体使用哪种方式根据窗口特点,在自动模式下:当是以下窗口是使用wgc 否则使用bltblt,

static const char *wgc_partial_match_classes[] = {
	"Chrome",
	"Mozilla",
	NULL,
};

static const char *wgc_whole_match_classes[] = {
	"ApplicationFrameWindow",
	"Windows.UI.Core.CoreWindow",
	"XLMAIN",        /* excel*/
	"PPTFrameClass", /* powerpoint */
	"OpusApp",       /* word */
	NULL,
};
choose_method(enum window_capture_method method, bool wgc_supported,
	      const char *current_class)
{
	if (!wgc_supported)
		return METHOD_BITBLT;

	if (method != METHOD_AUTO)
		return method;

	if (!current_class)
		return METHOD_BITBLT;

	const char **class = wgc_partial_match_classes;
	while (*class) {
		if (astrstri(current_class, *class) != NULL) {
			return METHOD_WGC;
		}
		class ++;
	}

	class = wgc_whole_match_classes;
	while (*class) {
		if (astrcmpi(current_class, *class) == 0) {
			return METHOD_WGC;
		}
		class ++;
	}

	return METHOD_BITBLT;
}

采集方式如上;

 WGC本质是使用到d3d11采集,GPU

 

 Windows.Graphics.Capture Namespace - Windows UWP applications | Microsoft Learn

E:\opensrc\obs_src_19041\obs-studio-19141\obs-studio\plugins\win-capture\window-capture.c

 

 桌面采集

 Advances to the display Infrastructure - Windows drivers | Microsoft Learn

Desktop duplication - Windows drivers | Microsoft Learn 

 

两种采集方式:

enum display_capture_method {
	METHOD_AUTO,
	METHOD_DXGI,
	METHOD_WGC,
};

采集方式的选择:

bool obs_module_load(void)
{
	struct win_version_info ver;
	bool win8_or_above = false;
	char *config_dir;

     //1 确定windows 版本
	struct win_version_info win1903 = {
		.major = 10, .minor = 0, .build = 18362, .revis = 0};

	config_dir = obs_module_config_path(NULL);
	if (config_dir) {
		os_mkdirs(config_dir);
		bfree(config_dir);
	}

	get_win_ver(&ver);

    // 2 win8
	win8_or_above = ver.major > 6 || (ver.major == 6 && ver.minor >= 2);


    // 3 使用d3d
	obs_enter_graphics();
	graphics_uses_d3d11 = gs_get_device_type() == GS_DEVICE_DIRECT3D_11;
	obs_leave_graphics();

    // 4 支持wgc
	if (graphics_uses_d3d11)
		wgc_supported = win_version_compare(&ver, &win1903) >= 0;

    // 5 选择使用duplicator_capture_info (wgc)或monitor_capture_info(gdi)  
	if (win8_or_above && graphics_uses_d3d11)
		obs_register_source(&duplicator_capture_info);
	else
		obs_register_source(&monitor_capture_info);

	obs_register_source(&window_capture_info);

	char *config_path = obs_module_config_path(NULL);

	init_hook_files();
	init_hooks_thread =
		CreateThread(NULL, 0, init_hooks, config_path, 0, NULL);
	obs_register_source(&game_capture_info);

	return true;
}

 E:\opensrc\obs_src_19041\obs-studio-19141\obs-studio\plugins\win-capture\duplicator-monitor-capture.c

static void duplicator_capture_tick(void *data, float seconds)
{
	struct duplicator_capture *capture = data;

	/* completely shut down monitor capture if not in use, otherwise it can
	 * sometimes generate system lag when a game is in fullscreen mode */
	if (!obs_source_showing(capture->source)) {
		if (capture->showing) {
			obs_enter_graphics();
			free_capture_data(capture);
			obs_leave_graphics();

			capture->showing = false;
		}
		return;
	}

	/* always try to load the capture immediately when the source is first
	 * shown */
	if (!capture->showing) {
		capture->reset_timeout = RESET_INTERVAL_SEC;
	}

	obs_enter_graphics();
    
    // 采集模式选择      
	if (capture->method == METHOD_WGC) {//wgc
		if (capture->reset_wgc && capture->capture_winrt) {
			capture->exports.winrt_capture_free(
				capture->capture_winrt);
			capture->capture_winrt = NULL;
			capture->reset_wgc = false;
			capture->reset_timeout = RESET_INTERVAL_SEC;
		}

		if (!capture->capture_winrt) {
			capture->reset_timeout += seconds;

			if (capture->reset_timeout >= RESET_INTERVAL_SEC) {
				capture->capture_winrt =
					capture->exports
						.winrt_capture_init_monitor(
							capture->capture_cursor,
							capture->handle);

				capture->reset_timeout = 0.0f;
			}
		}
	} else {
		if (capture->capture_winrt) {
			capture->exports.winrt_capture_free(
				capture->capture_winrt);
			capture->capture_winrt = NULL;
		}

		if (!capture->duplicator) {
			capture->reset_timeout += seconds;
             //桌面复制
			if (capture->reset_timeout >= RESET_INTERVAL_SEC) {
				capture->duplicator = gs_duplicator_create(
					capture->dxgi_index);

				capture->reset_timeout = 0.0f;
			}
		}

		if (capture->duplicator) {
			if (capture->capture_cursor)
				cursor_capture(&capture->cursor_data);

			if (!gs_duplicator_update_frame(capture->duplicator)) {
				free_capture_data(capture);

			} else if (capture->width == 0) {
				reset_capture_data(capture);
			}
		}
	}

	obs_leave_graphics();

	if (!capture->showing)
		capture->showing = true;

	UNUSED_PARAMETER(seconds);
}

默认会走到桌面复制

 

 当选择 windows 10 則使用wgc

 BitBlt x264 采集

四个线程:

  • video_thread:视频编码和输出线程,后面简称“video 线程,负责视频编码和输出
  • obs_graphics_thread:视频渲染线程,后面简称“graphics 线程,也就是源合成音视频、生成原始视频帧、显示到窗口(预览)等功能
  • audio_thread: 音频编码和输出线程,后面简称“audio 线程
  • CaptureThread:音频采集

  当点击开始录制按钮时,在ffmpeg_mux_start 中,raw_active被设置为ture,obs_graphics_thread post信号量,video_thread接收到信号量后,编码输出

点击开始录制后:

video_thread线程的创建过程:

ResetVideo

   AttemptToResetVideo

        --obs_reset_video

             -- obs_init_video 

                       --video_output_open(&video->video, &vi);

                                    --pthread_create(&out->thread, NULL, video_thread   创建video线程

                        --pthread_create(&video->video_thread, NULL, obs_graphics_thread, obs);创建视频渲染线程

在obs_graphics_thread中,通过while (obs_graphics_thread_loop(&context)) 处理任务:

当帧准备好后,通过 output_video_data 调用video_output_unlock_frame  发送信号量

通知video线程开始工作 :

video线程拿到信号量进入while循环

video_output_cur_frame 处理当前视频帧

 

scale_video_output 做scale 处理,采集到rgb转yuv,等

input->callback是static void receive_video(void *param, struct video_data *frame)

做视频编码:

 

进入具体的编码方式:obs_x264_encode

static bool obs_x264_encode(void *data, struct encoder_frame *frame,
			    struct encoder_packet *packet,
			    bool *received_packet)
{
	struct obs_x264 *obsx264 = data;
	x264_nal_t *nals;
	int nal_count;
	int ret;
	x264_picture_t pic, pic_out;

	if (!frame || !packet || !received_packet)
		return false;

	if (frame)
		init_pic_data(obsx264, &pic, frame);

	ret = x264_encoder_encode(obsx264->context, &nals, &nal_count,
				  (frame ? &pic : NULL), &pic_out);
	if (ret < 0) {
		warn("encode failed");
		return false;
	}

	*received_packet = (nal_count != 0);
	parse_packet(obsx264, packet, nals, nal_count, &pic_out);

	return true;
}

 其中:x264_encoder_encode 是依赖中的接口

E:\opensrc\obs_src_19041\dependencies2019\win32\include\x264.h

 

结束录制

主线程调用堆栈:

     obs-ffmpeg.dll!ffmpeg_mux_stop(void * data, unsigned __int64 ts) 

     obs.dll!obs_output_actual_stop(obs_output * output, bool force, unsigned __int64 ts) 

     obs.dll!obs_output_stop(obs_output * output) 

     obs32.exe!SimpleOutput::StopRecording(bool force) 

     obs32.exe!OBSBasic::StopRecording()

ucrtbased.dll线程 obs_x264_destroy:x64销毁

 音频设备初始化以及音频采集线程创建

E:\opensrc\obs_src_19041\obs-studio-19141\obs-studio\plugins\win-wasapi\win-wasapi.cpp


static void *CreateWASAPISource(obs_data_t *settings, obs_source_t *source,
				bool input)
{
	try {
		return new WASAPISource(settings, source, input);
	} catch (const char *error) {
		blog(LOG_ERROR, "[CreateWASAPISource] %s", error);
	}

	return nullptr;
}

 com初始化

WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_,
			   bool input)
	: source(source_), isInputDevice(input)
{
	UpdateSettings(settings);

	stopSignal = CreateEvent(nullptr, true, false, nullptr);
	if (!stopSignal.Valid())
		throw "Could not create stop signal";

	receiveSignal = CreateEvent(nullptr, false, false, nullptr);
	if (!receiveSignal.Valid())
		throw "Could not create receive signal";

	Start();
}

inline void WASAPISource::Start()
{
	if (!TryInitialize()) {
		blog(LOG_INFO,
		     "[WASAPISource::WASAPISource] "
		     "Device '%s' not found.  Waiting for device",
		     device_id.c_str());
		Reconnect();
	}
}

......

void WASAPISource::Initialize()
{
	HRESULT res;
    //初始化com
	res = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
			       CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),
			       (void **)enumerator.Assign());
	if (FAILED(res))
		throw HRError("Failed to create enumerator", res);

	if (!InitDevice())
		return;

	device_name = GetDeviceName(device);

	if (!notify) {
		notify = new WASAPINotify(this);
		enumerator->RegisterEndpointNotificationCallback(notify);
	}

	HRESULT resSample;
	IPropertyStore *store = nullptr;
	PWAVEFORMATEX deviceFormatProperties;
	PROPVARIANT prop;
	resSample = device->OpenPropertyStore(STGM_READ, &store);
	if (!FAILED(resSample)) {
		resSample =
			store->GetValue(PKEY_AudioEngine_DeviceFormat, &prop);
		if (!FAILED(resSample)) {
			if (prop.vt != VT_EMPTY && prop.blob.pBlobData) {
				deviceFormatProperties =
					(PWAVEFORMATEX)prop.blob.pBlobData;
				device_sample = std::to_string(
					deviceFormatProperties->nSamplesPerSec);
			}
		}

		store->Release();
	}

	InitClient();
	if (!isInputDevice)
		InitRender();
	InitCapture();
}

创建音频采集线程CaptureThread

 

bool WASAPISource::ProcessCaptureData()
{
	HRESULT res;
	LPBYTE buffer;
	UINT32 frames;
	DWORD flags;
	UINT64 pos, ts;
	UINT captureSize = 0;

	while (true) {
         // 获取采集的数据包大小
		res = capture->GetNextPacketSize(&captureSize);

		if (FAILED(res)) {
			if (res != AUDCLNT_E_DEVICE_INVALIDATED)
				blog(LOG_WARNING,
				     "[WASAPISource::GetCaptureData]"
				     " capture->GetNextPacketSize"
				     " failed: %lX",
				     res);
			return false;
		}

		if (!captureSize)
			break;

        //获取音频数据
		res = capture->GetBuffer(&buffer, &frames, &flags, &pos, &ts);
		if (FAILED(res)) {
			if (res != AUDCLNT_E_DEVICE_INVALIDATED)
				blog(LOG_WARNING,
				     "[WASAPISource::GetCaptureData]"
				     " capture->GetBuffer"
				     " failed: %lX",
				     res);
			return false;
		}

		obs_source_audio data = {};
		data.data[0] = (const uint8_t *)buffer;
		data.frames = (uint32_t)frames;
		data.speakers = speakers;
		data.samples_per_sec = sampleRate;
		data.format = format;
		data.timestamp = useDeviceTiming ? ts * 100 : os_gettime_ns();

		if (!useDeviceTiming)
			data.timestamp -= util_mul_div64(frames, 1000000000ULL,
							 sampleRate);

       //音频数据处理
		obs_source_output_audio(source, &data);

		capture->ReleaseBuffer(frames);
	}

	return true;
}

 audio_thread

 

 

 

 

static void input_and_output(struct audio_output *audio, uint64_t audio_time,
			     uint64_t prev_time)
{
	size_t bytes = AUDIO_OUTPUT_FRAMES * audio->block_size;
	struct audio_output_data data[MAX_AUDIO_MIXES];
	uint32_t active_mixes = 0;
	uint64_t new_ts = 0;
	bool success;

	memset(data, 0, sizeof(data));

#ifdef DEBUG_AUDIO
	blog(LOG_DEBUG, "audio_time: %llu, prev_time: %llu, bytes: %lu",
	     audio_time, prev_time, bytes);
#endif

	/* get mixers */
	pthread_mutex_lock(&audio->input_mutex);
	for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
		if (audio->mixes[i].inputs.num)
			active_mixes |= (1 << i);
	}
	pthread_mutex_unlock(&audio->input_mutex);

	/* clear mix buffers */
	for (size_t mix_idx = 0; mix_idx < MAX_AUDIO_MIXES; mix_idx++) {
		struct audio_mix *mix = &audio->mixes[mix_idx];

		memset(mix->buffer, 0, sizeof(mix->buffer));

		for (size_t i = 0; i < audio->planes; i++)
			data[mix_idx].data[i] = mix->buffer[i];
	}

	/* get new audio data */
	success = audio->input_cb(audio->input_param, prev_time, audio_time,
				  &new_ts, active_mixes, data);
	if (!success)
		return;

	/* clamps audio data to -1.0..1.0 */
	clamp_audio_output(audio, bytes);

	/* output */
	for (size_t i = 0; i < MAX_AUDIO_MIXES; i++)
		do_audio_output(audio, i, new_ts, AUDIO_OUTPUT_FRAMES);
}

最后调用到do_audio_output

static const char *receive_audio_name = "receive_audio";
static void receive_audio(void *param, size_t mix_idx, struct audio_data *in)
{
	profile_start(receive_audio_name);

	struct obs_encoder *encoder = param;
	struct audio_data audio = *in;

	if (!encoder->first_received) {
		encoder->first_raw_ts = audio.timestamp;
		encoder->first_received = true;
		clear_audio(encoder);
	}

	if (audio_pause_check(&encoder->pause, &audio, encoder->samplerate))
		goto end;

	if (!buffer_audio(encoder, &audio))
		goto end;

	while (encoder->audio_input_buffer[0].size >=
	       encoder->framesize_bytes) {
		if (!send_audio_data(encoder)) {
			break;
		}
	}

	UNUSED_PARAMETER(mix_idx);

end:
	profile_end(receive_audio_name);
}

 send_audio_data 中进行编码

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

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

相关文章

【Cache】Rsync远程同步

文章目录 一、rsync 概念二、rysnc 服务器部署1. 环境配置2. rysnc 同步源服务器2.1 安装 rsync2.2 建立 rsyncd.conf 配置文件2.3 创建数据文件&#xff08;账号密码&#xff09;2.4 启动服务2.5 数据配置 3. rysnc 客户端3.1 设置同步方法一方法二 3.2 免交互设置 4. rysnc 认…

17-Linux环境搭建——使用云服务器

目录 1.环境搭建的主要4种方式&#xff1a; 2.使用云服务器 2.1.腾讯云 PS&#xff1a;轻量应用服务器和云服务器的区别 PS&#xff1a;首单特惠 2.2.阿里云 2.3.其他类似 3.使用终端软件连接到 Linux 3.1.什么是终端软件&#xff1f; 3.2.使用 XShell PS&#xff1…

No5.精选前端面试题,享受每天的挑战和学习

文章目录 说一下虚拟dom&#xff0c;结合vue和react白屏问题怎么优化java和javascript的区别Vue3.0和2.0对比&#xff0c;哪些方面更加出色 说一下虚拟dom&#xff0c;结合vue和react 虚拟DOM&#xff08;Virtual DOM&#xff09;是JavaScript框架如Vue和React中的一个重要概念…

CSDN博客运营团队2023年H1总结

一、运营工作的指导思路 CSDN的核心价值之一是帮助开发者/技术人快速的学习成长和解决技术问题。这个核心价值的建立离不开无数开发者的无私贡献&#xff0c;而运营工作的核心就是要保证这个价值链不断放大&#xff1a;让内容贡献者体验更好&#xff0c;付出有回报&#xff0c…

黑盟菜鸟剪辑短视频助手是什么

今天我们来讲一下视频综合处理功能&#xff0c;首先我们来打开软件主界面&#xff0c;通过模块化功能合集视频综合处理进入我们的这个功能。首先我们来看一下功能的布局&#xff0c;左边是导入视频的地方&#xff0c;右边是功能区&#xff0c;这里总共包括 32 种功能&#xff0…

使用随便测测平台-做接口测试

目录 接口数据的来源 导出har文件 导入har文件 转化为用例 提取数据进行替换 如何选择哪些数据需要替换呢&#xff1f; Url Params、Data ​编辑进行替换操作 断言-冒烟 断言-详细 测试报告 结束 接口数据的来源 1、可通过charles工具&#xff0c;录制好接口操作&…

Jmeter使用之:高效组织接口自动化用例技巧

目录 前言&#xff1a; 1 2 3 4 5 6 前言&#xff1a; 在进行接口自动化测试时&#xff0c;JMeter是一个非常常用的工具。它具有强大的性能和扩展性&#xff0c;可以用于模拟大量并发用户的请求&#xff0c;对接口进行压力测试和性能监测。 1 善用“逻辑控制器”中的…

Android Studio实现内容丰富的安卓图书馆座位图书预约系统

如需源码可以添加q-------3290510686&#xff0c;也有演示视频演示具体功能&#xff0c;源码不免费&#xff0c;尊重创作&#xff0c;尊重劳动。 项目编号109 1.开发环境 android stuido jdk1.8 eclipse mysql tomcat 2.功能介绍 安卓端&#xff1a; 1.注册登录 2.查看公告 3.查…

智能邮件过滤与分类工具评估:AI办公工具准确性和智能性一览

在现代办公环境中&#xff0c;处理大量的电子邮件是一项常见而繁琐的任务。为了提高工作效率&#xff0c;智能邮件过滤与分类工具应运而生。这些AI办公工具承诺通过准确性和智能性来帮助我们更好地管理和组织电子邮件。然而&#xff0c;我们需要对它们的表现进行评估&#xff0…

Redis概述及安装、使用和管理(烦心事永久打烊)

文章目录 一、NoSQL非关系型数据库1.NoSQL概述2.关系型数据库和非关系型数据库区别&#xff08;1&#xff09;数据存储方式不同&#xff08;2&#xff09;扩展方式不同&#xff08;3&#xff09;对事务性的支持不同 3.非关系型数据库使用场景 二、Redis概述1.简介2.优点3.Redis…

Spark计算引擎介绍

1. Spark是什么 Apache Spark是专为大规模数据处理而设计的快速通用的计算引擎。 Spark是加州大学伯克利分校的AMP实验室&#xff08;Algorithms, Machines and People Lab&#xff09;开源的类Hadoop MapReduce的通用并行框架&#xff0c;拥有Hadoop MapReduce所具有的优点&…

1、hadoop集群搭建

1、 Hadoop运行环境搭建 1.1、修改主机名和hosts文件 1&#xff09;修改主机名称 sudo hostnamectl set-hostname <newhostname>sudo hostnamectl set-hostname hadoop101 sudo hostnamectl set-hostname hadoop102 sudo hostnamectl set-hostname hadoop103 2&#…

C# ref out的使用与区别

Ref 和 out可以理解为类似的传址引用。 在函数需要外部传入一个变量名&#xff0c;然后在程序内部可以将这个值进行修改&#xff0c;典型的传址引用&#xff01;在定义时必要加ref或out说明&#xff01; Ref和Out的区别&#xff1a; ref 关键字使参数按引用传递。其效果是&am…

炼石白小勇:从业务视角重塑数据安全

2023年6月27日&#xff0c;由中国信息通信研究院、中国通信标准化协会主办&#xff0c;中国通信标准化协会大数据技术标准推进委员会承办&#xff0c;数据安全推进计划支持的2023年大数据产业发展大会-数据安全高质量发展论坛在北京成功召开。 本次论坛由中国信通院云计算与大数…

maven 环境配置踩坑

今晚在跟着视频学习spring的时候&#xff0c;创建maven工程&#xff0c;一直提示Sync 下载异常。搞了一晚上终于搞定了环境。下面给出一下今晚的总结。 1、确保maven安装并配置好环境变量。 下载并安装maven后&#xff0c;还需要在电脑上配置maven的环境变量。这部分参考网络教…

关于windows本机开放端口后,同一个网络下的其他电脑telnet不通的解决办法

前提&#xff1a;电脑A与电脑B均在同一个wifi下。 本地电脑A 启动了一个kong网关&#xff08;不管是什么应用&#xff0c;只要开启了一个监听端口就行&#xff09;&#xff0c;并且将docker里面的8000端口映射到本机的8000端口&#xff0c; 此时在电脑A上的命令行运行&#xf…

2023 IJCAI YES 青年精英学术大会正式开幕:群贤毕至,开启为期三天的智慧盛宴

内容一览&#xff1a; 2019 年&#xff0c;艾伦人工智能研究所发布《中国在人工智能研究领域超越美国》研究报告。这份报告反映了中国人工智能领域从「数」到「质」的飞跃&#xff0c;这不仅得益于国家政策的推动&#xff0c;更因为众多国内高校的不懈探索。 当前&#xff0c;人…

HTML和CSS配合制作一个简单的登录界面

HTML和CSS配合制作一个简单的登录界面 界面HTMLCSS解释语法 界面 HTML <!DOCTYPE html> <html lang"en"> <head><title>篮球世界</title><meta charset"UTF-8"><link type"text/css" rel"styleshe…

关注电动汽车(EV)能效水平 提高电动汽车续航能力

电动汽车&#xff08;EV&#xff09;近些年发展迅猛&#xff0c;已被汽车业内普遍认为是未来汽车发展的新方向&#xff0c;但现如今电动汽车仍然存在一些短板&#xff0c;导致其还无法替代传统燃油车。对此&#xff0c;首先想到的肯定就是电动车的续航问题。其实解决电动车续航…

揭秘Vue 2中的$nextTick:等待DOM更新的神奇时刻!

文章目录 1. 队列机制2. 异步执行3. 标记更新4. 下一次 DOM 更新循环5. 触发回调函数 在 Vue 2 中&#xff0c;$nextTick 是一个异步方法&#xff0c;用于在下次 DOM 更新循环结束后执行回调函数。 它的原理可以解析如下&#xff1a; 1. 队列机制 Vue 2 维护了一个队列&…