群控系统是市场上比较流行的营销工具,很多人在很好的利用这一工具后掘到了一桶桶金。其应用的行业也是十分广泛,只要是需要网络营销推广,需要获取大量粉丝的行业,短视频需要上人气,闲鱼自动上货等等,都能用到该工具。本人带领技术团队也开发了一套完整的群控系统,下面就群控系统的实现原理,以及部分核心代码供大家参考。
不同于市场上的其他群控系统,我们研发的群控系统是可以运行在任何电脑上的,不用再购买另外一台服务器,对群控系统的操作便捷性以及实用性都做了极大的优化。该系统的实现主要分为几个部分,一是pc端操作软件,二是adb底层传输,三是手机控制端(包含脚本引擎和实时画面传输模块)。如果是想控制授权客户,还有一个授权系统。
pc端操作软件,主要是把所有手机的界面投射到电脑上,实现手机和电脑的同步操作,以及一些与手机互动操作的功能,例如:一键锁屏、一键解锁、上传文件、安装APK、卸载APK、一键截图、手机音量调节等等。其中比较核心的问题是手机界面实时投射到电脑上,我们在安卓端采用了H264视频录屏模式和截图模式,采用Socket传输,在进行了图像压缩,质量压缩等优化后,目前传输基本零延迟,无任何卡顿现象。
手机端主要使用的是Uiautomator2.0,使用该方案比其他使用Uiautomator的群控获取界面信息更快,即使是动态界面,一样可以快速获取,这是其他群控锁不能比拟的,而且我们在手机端也加入了opencv图像识别引擎,保证了一些无法通过自动化测试工具获取界面元素的情况下也可以实现自动化脚本。
以下是手机端的截图模式的核心实现代码,核心代码为:
public final void onImageAvailable(ImageReader imageReader) {
try {
Image acquireLatestImage = imageReader.acquireLatestImage();
try {
int bufferLen = 0;
if (acquireLatestImage != null) {
long currentTimeMillis = System.currentTimeMillis();
Image.Plane[] planes = acquireLatestImage.getPlanes();
Buffer buffer = planes[0].getBuffer();
this.aa.pixelStride = planes[0].getPixelStride();
this.aa.rowStride = planes[0].getRowStride();
acquireLatestImage.close();
int x;
Bitmap createBitmap;
byte[] tmpBuffer;
if (this.aa.isNormalSpeed) {
if (System.currentTimeMillis() - this.aa.lastSendTimestamps > 200) {
this.aa.lock.lock();
this.aa.imageBuffer = null;
this.aa.lock.unlock();
x = this.aa.rowStride - (this.aa.pixelStride * this.aa.widthImg);
createBitmap = Bitmap.createBitmap((x / this.aa.pixelStride) + this.aa.widthImg, this.aa.heighImg, Config.ARGB_8888);
createBitmap.copyPixelsFromBuffer(buffer);
tmpBuffer = a.compressBitmap(this.aa.shrinkBitmap(createBitmap, 0.4f), 75);
bufferLen = tmpBuffer.length;
this.aa.lastSendTimestamps = System.currentTimeMillis();
this.aa.sendBackToPc(tmpBuffer, (byte) 1);
}
else {
this.aa.lock.lock();
this.aa.imageBuffer = buffer;
this.aa.lock.unlock();
}
Log.i(TAG, new StringBuilder("发送一帧 大小:").append(bufferLen).append(" 耗时:").append(System.currentTimeMillis() - currentTimeMillis).toString());
}
else if (this.aa.isImageModel) {
//高速模式,并且是图像模式
//2019.12.25东东修改,使用大图像处理线程发送图像
if(System.currentTimeMillis() - this.aa.lastMaxImgSendTimestamps > 200)
{
try {
this.aa.lock.lock();
this.aa.imageBuffer = null;
this.aa.lock.unlock();
x = this.aa.rowStride - (this.aa.pixelStride * this.aa.widthImg);
createBitmap = Bitmap.createBitmap((x / this.aa.pixelStride) + this.aa.widthImg, this.aa.heighImg, Config.ARGB_8888);
createBitmap.copyPixelsFromBuffer(buffer);
tmpBuffer = a.compressBitmap(this.aa.shrinkBitmap(createBitmap, 1.0f), this.aa.quality);
bufferLen = tmpBuffer.length;
this.aa.sendBackToPc(tmpBuffer, (byte) 2);
}
catch (Throwable th) {
th.printStackTrace();
}
}
else
{
this.aa.lock2.lock();
this.aa.imageMaxBuffer = buffer;
this.aa.lock2.unlock();
}
Log.i(TAG, new StringBuilder("发送一帧 大小:").append(bufferLen).append(" 耗时:").append(System.currentTimeMillis() - currentTimeMillis).toString());
}
}
} catch (Throwable th2) {
Log.e(TAG, "onImageAvailable: " + th2.getLocalizedMessage());
}
finally {
try {
if (acquireLatestImage != null) {
acquireLatestImage.close();
}
} catch (Exception th3) {
}
}
}
catch (Exception th4) {
}
下面是H264录屏模式的代码:
//启动屏幕录制
private synchronized String startScreenRecorderH264(int width, int height, int bitrate) {
try {
if (this.mediaCodec != null) {
this.mediaCodec.stop();
this.mediaCodec.release();
this.mediaCodec = null;
System.gc();
}
MediaFormat createVideoFormat = MediaFormat.createVideoFormat("video/avc", this.widthImg, this.heighImg);
if (createVideoFormat == null) {
Log.e(TAG,this.heighImg + " 开始录制时出错! createVideoFormat 为null" + this.widthImg);
return "开始录制时出错createVideoFormat无法启动" ;
}
createVideoFormat.setInteger("color-format", 2130708361);
createVideoFormat.setInteger("sample-rate", 0);
createVideoFormat.setInteger("bitrate", bitrate);
createVideoFormat.setInteger("frame-rate", 20);
createVideoFormat.setInteger("i-frame-interval", 60);
createVideoFormat.setInteger("width", width);
createVideoFormat.setInteger("height", height);
Log.e(TAG,"created video format: " + createVideoFormat);
this.mediaCodec = MediaCodec.createEncoderByType("video/avc");
if (this.mediaCodec == null) {
Log.e(TAG,"开始录制时出错! mediaCodec 为null");
return "开始录制时出错 mediaCodec 为null" ;
}
this.mediaCodec.configure(createVideoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
this.surfaceVideo = this.mediaCodec.createInputSurface();
System.out.print("created input surface: " + this.surfaceVideo + "\r\n");
this.mediaCodec.start();
Log.i(TAG,"编码器启动成功!");
this.iBinder = 驶("video2", this.surfaceVideo,this.widthImg,this.heighImg,this.width,this.heigh);
return null;
}
catch (Throwable th) {
th.printStackTrace();
String s = new StringBuilder("开始录制时出错:").append(th.getCause()).append(th.getStackTrace()[0].getLineNumber()).toString();
Log.e(TAG, s);
return s;
}
}
/**
*===========H264屏幕录制线程函数==========
*
**/
static void changeScreenH264(a aVar) {
while (true) {
// 如果是NormalSpeed,则空循环,由image格式去刷新pc端小屏幕
if (aVar.isNormalSpeed) {
try {
Thread.sleep(100);
}
catch (Throwable th) {
th.printStackTrace();
}
}
else {
try {
if (aVar.mediaCodec == null) {
Log.e(TAG, "mediaCodec 是 null");
try {
Thread.sleep(2000);
}
catch (Exception e) {
}
continue;
}
int dequeueOutputBuffer = aVar.mediaCodec.dequeueOutputBuffer(aVar.bufferInfo, -1);
if (dequeueOutputBuffer == -2) {
Log.i(TAG, new StringBuilder("New format ").append(aVar.mediaCodec.getOutputFormat()).toString());
}
else if (dequeueOutputBuffer == -1) {
try {
Thread.sleep(10);
}
catch (Exception e) {
}
}
else if (dequeueOutputBuffer >= 0) {
ByteBuffer byteBuffer = aVar.mediaCodec.getOutputBuffers()[dequeueOutputBuffer];
if (aVar.bufferInfo.size == 0) {
byteBuffer = null;
}
if (byteBuffer != null) {
int size = aVar.bufferInfo.size;
long j = aVar.bufferInfo.presentationTimeUs;
aVar.sendH264ToPc(byteBuffer, size, aVar.bufferInfo.flags);
byteBuffer.clear();
}
aVar.mediaCodec.releaseOutputBuffer(dequeueOutputBuffer, false);
}
}
catch (Exception e2)
{
try
{
Thread.sleep(2000);
}
catch (Exception e)
{
}
Log.e(TAG, new StringBuilder("录制失败:").append(e2.getLocalizedMessage()).append(getStackMsg(e2)).toString());
}
}
}
}