RK Android7.1 Camera2 预览、内置客户SDK预览
一.预览
1.SurfaceView
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 | <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:orientation= "vertical" > <SurfaceView android:id= "@+id/sfv_preview" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:layout_weight= "1" /> </LinearLayout> |
AndroidManifest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?xml version= "1.0" encoding= "utf-8" ?> <manifest xmlns:android= "http://schemas.android.com/apk/res/android" package = "com.gatsby.ypcamerdemo" > <uses-permission android:name= "android.permission.CAMERA" /> <uses-permission android:name= "android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:allowBackup= "true" android:icon= "@mipmap/ic_launcher" android:label= "@string/app_name" android:roundIcon= "@mipmap/ic_launcher_round" android:supportsRtl= "true" android:theme= "@style/Theme.YpCamerDemo" > <activity android:name= ".MainActivity" > <intent-filter> <action android:name= "android.intent.action.MAIN" /> <category android:name= "android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> |
Mainactivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | package com.gatsby.ypcamerdemo; import android.hardware.Camera; import android.os.Bundle; import android.view.SurfaceHolder; import android.view.SurfaceView; import androidx.appcompat.app.AppCompatActivity; import java.io.IOException; public class MainActivity extends AppCompatActivity { private SurfaceView sfv_preview; private Camera camera = null ; private SurfaceHolder.Callback cpHolderCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { startPreview(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { stopPreview(); } }; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); bindViews(); } private void bindViews() { sfv_preview = (SurfaceView) findViewById(R.id.sfv_preview); sfv_preview.getHolder().addCallback(cpHolderCallback); } //开始预览 private void startPreview() { camera = Camera.open(); try { camera.setPreviewDisplay(sfv_preview.getHolder()); camera.setDisplayOrientation( 0 ); camera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } //停止预览 private void stopPreview() { camera.stopPreview(); camera.release(); camera = null ; } } |
2.TextureView
- CameraManager: 管理手机上的所有摄像头设备,它的作用主要是获取摄像头列表和打开指定的摄像头
- CameraDevice: 具体的摄像头设备,它有一系列参数(预览尺寸、拍照尺寸等),可以通过CameraManager的getCameraCharacteristics()方法获取。它的作用主要是创建CameraCaptureSession和CaptureRequest
- CameraCaptureSession: 相机捕获会话,用于处理拍照和预览的工作(很重要)
- CaptureRequest: 捕获请求,定义输出缓冲区以及显示界面(TextureView或SurfaceView)等
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 | <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:orientation= "vertical" > <TextureView android:id= "@+id/textureview" android:layout_width= "match_parent" android:layout_height= "match_parent" /> </LinearLayout> |
Mainactivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | package com.gatsby.textureviewtest; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import android.view.Surface; import android.view.TextureView; import androidx.appcompat.app.AppCompatActivity; import java.util.Arrays; public class MainActivity extends AppCompatActivity { private TextureView textureView; private String TAG= "Gatsby" ; private HandlerThread mThreadHandler; private Handler mHandler; private CaptureRequest.Builder mPreviewBuilder; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); mThreadHandler = new HandlerThread( "CAMERA2" ); mThreadHandler.start(); mHandler = new Handler(mThreadHandler.getLooper()); textureView= (TextureView) findViewById(R.id.textureview); textureView.setSurfaceTextureListener(textureListener); } private TextureView.SurfaceTextureListener textureListener= new TextureView.SurfaceTextureListener() { @SuppressLint ( "MissingPermission" ) @TargetApi (Build.VERSION_CODES.LOLLIPOP) @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { Log.e(TAG, "可用" ); //CameraManaer 摄像头管理器,用于检测摄像头,打开系统摄像头 CameraManager cameraManager = (CameraManager) getSystemService(CAMERA_SERVICE); try { String[] CameraIdList=cameraManager.getCameraIdList(); //获取可用相机列表 Log.e(TAG, "可用相机的个数是:" +CameraIdList.length); //获取某个相机(摄像头特性) CameraCharacteristics cameraCharacteristics=cameraManager.getCameraCharacteristics(CameraIdList[ 0 ]); cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); //检查支持 cameraManager.openCamera(CameraIdList[ 0 ],mCameraDeviceStateCallback, mHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) { Log.e(TAG, "change" ); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { Log.e(TAG, "destroy" ); return false ; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { Log.e(TAG, "update" ); } }; //CameraDeviceandroid.hardware.Camera也就是Camera1的Camera private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { try { startPreview(camera); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onDisconnected(CameraDevice camera) { } @Override public void onError(CameraDevice camera, int error) { } }; /** * @param camera * @throws CameraAccessException * 开始预览 */ private void startPreview(CameraDevice camera) throws CameraAccessException { SurfaceTexture texture = textureView.getSurfaceTexture(); texture.setDefaultBufferSize(textureView.getWidth(), textureView.getHeight()); Surface surface = new Surface(texture); try { //CameraRequest表示一次捕获请求,用来对z照片的各种参数设置,比如对焦模式、曝光模式等。CameraRequest.Builder用来生成CameraRequest对象 mPreviewBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); } catch (CameraAccessException e) { e.printStackTrace(); } mPreviewBuilder.addTarget(surface); camera.createCaptureSession(Arrays.asList(surface), mSessionStateCallback, mHandler); } //CameraCaptureSession 这个对象控制摄像头的预览或者拍照 //setRepeatingRequest()开启预览,capture()拍照 //StateCallback监听CameraCaptureSession的创建 private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { Log.e(TAG, "相机创建成功!" ); try { session.capture(mPreviewBuilder.build(), mSessionCaptureCallback, mHandler); //拍照 session.setRepeatingRequest(mPreviewBuilder.build(), mSessionCaptureCallback, mHandler); //返回结果 } catch (CameraAccessException e) { e.printStackTrace(); Log.e(TAG, "这里异常" ); } } @Override public void onConfigureFailed(CameraCaptureSession session) { Log.e(TAG, "相机创建失败!" ); } }; //CameraCaptureSession.CaptureCallback监听拍照过程 private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { Log.e(TAG, "这里接受到数据" +result.toString()); } @Override public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult){ }}; } |
二.Android camera2预览界面流程 https://blog.csdn.net/fireness/article/details/50594706
2.1.找到布局
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <com.android.camera.ui.MainActivityLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:id= "@+id/activity_root_view" android:layout_width= "match_parent" android:layout_height= "match_parent" android:background= "@null" > <include layout= "@layout/camera" /> <include layout= "@layout/camera_filmstrip" /> <com.android.camera.ui.ModeTransitionView android:id= "@+id/mode_transition_view" android:visibility= "gone" android:layout_width= "match_parent" android:layout_height= "match_parent" /> </com.android.camera.ui.MainActivityLayout> |
预览布局 camera.xml
1 2 3 4 5 6 7 8 9 10 11 | <FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:id= "@+id/camera_app_root" android:background= "@android:color/black" android:layout_width= "match_parent" android:layout_height= "match_parent" > <TextureView android:id= "@+id/preview_content" android:layout_width= "match_parent" android:layout_height= "match_parent" /> |
2.1.CameraActivity.java onCreateTasks() 准备预览mCameraAppUI.prepareModuleUI();
packages\apps\Camera2\src\com\android\camera\app\CameraAppUI.java
1 2 3 4 5 6 7 8 | public void prepareModuleUI() { mController.getSettingsManager().addListener( this ); mModuleUI = (FrameLayout) mCameraRootView.findViewById(R.id.module_layout); mTextureView = (TextureView) mCameraRootView.findViewById(R.id.preview_content); mTextureViewHelper = new TextureViewHelper(mTextureView, mCaptureLayoutHelper, mController.getCameraProvider(), mController); mTextureViewHelper.setSurfaceTextureListener( this ); mTextureViewHelper.setOnLayoutChangeListener(mPreviewLayoutChangeListener); |
2.3.TextureViewHelper 初始化
packages\apps\Camera2\src\com\android\camera\TextureViewHelper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 | public TextureViewHelper(TextureView preview, CaptureLayoutHelper helper, CameraProvider cameraProvider, AppController appController) { mPreview = preview; mCameraProvider = cameraProvider; mPreview.addOnLayoutChangeListener( this ); mPreview.setSurfaceTextureListener( this ); mCaptureLayoutHelper = helper; mAppController = appController; mCameraModeId = appController.getAndroidContext().getResources() .getInteger(R.integer.camera_mode_photo); mCaptureIntentModeId = appController.getAndroidContext().getResources() .getInteger(R.integer.camera_mode_capture_intent); } |
还要去实现 implements TextureView.SurfaceTextureListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // Workaround for b/11168275, see b/10981460 for more details if (mWidth != 0 && mHeight != 0 ) { // Re-apply transform matrix for new surface texture updateTransform(); } if (mSurfaceTextureListener != null ) { mSurfaceTextureListener.onSurfaceTextureAvailable(surface, width, height); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { if (mSurfaceTextureListener != null ) { mSurfaceTextureListener.onSurfaceTextureSizeChanged(surface, width, height); } } |
packages\apps\Camera2\src\com\android\camera\PhotoUI.java
1 2 3 4 | @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mController.onPreviewUIReady(); } |
2.4.packages\apps\Camera2\src\com\android\camera\PhotoModule.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @Override public void onPreviewUIReady() { Log.i(TAG, "onPreviewUIReady" ); startPreview(); } private void startPreview() { if (mCameraDevice == null ) { Log.i(TAG, "attempted to start preview before camera device" ); // do nothing return ; } ………………………… Log.i(TAG, "startPreview" ); // If we're using API2 in portability layers, don't use startPreviewWithCallback() // b/17576554 CameraAgent.CameraStartPreviewCallback startPreviewCallback = new CameraAgent.CameraStartPreviewCallback() { @Override public void onPreviewStarted() { mFocusManager.onPreviewStarted(); PhotoModule. this .onPreviewStarted(); SessionStatsCollector.instance().previewActive( true ); if (mSnapshotOnIdle || (mAppController.getSettingsManager().getBoolean (SettingsManager.SCOPE_GLOBAL, Keys.KEY_SMILE_SHUTTER_ON) && DebugPropertyHelper.isSmileShutterAuto())) { mHandler.postDelayed(mDoSmileShutterRunnable, 2000 ); } } }; |
三.内置SDK 预览
3.1.客户demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | private var mSurfaceTexture: SurfaceTexture? = null private var mSurface: Surface? = null private val mSurfaceTextureListener: SurfaceTextureListener = object : SurfaceTextureListener { override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { mSurfaceTexture = surface } override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { mSurfaceTexture = surface } override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { mSurfaceTexture = surface return false } override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { mSurfaceTexture = surface } } private fun initView() { if (mSurface == null && mSurfaceTexture != null ) { mSurface = Surface(mSurfaceTexture) } mYpDevicesManager = YpDevicesManager.getInstance() mYpDevicesManager?.setContext( this ) mYpDevicesManager?.setCodereaderCameraView(mSurface) |
3.2.客户是特殊的USB3.0摄像头 走的不是 Camera open(int cameraId) 方法 而Camera2 预览走的流程是标准的 open方法
原计划是在Camera2 预览控件TextureView做动作 现在不走open 表示要大改预览框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 08 - 17 16 : 17 : 35.796 E/CameraService( 244 ): No camera be found ! check again... 08 - 17 16 : 17 : 35.796 I/CameraService( 244 ): CameraService process starting 08 - 17 16 : 17 : 35.797 I/CAM2PORT_AndCamAgntImp( 1442 ): Opening camera 0 with camera1 API 08 - 17 16 : 17 : 35.797 D/CameraHal( 244 ): Calling process is: com.android.camera2 08 - 17 16 : 17 : 35.798 I/CameraService( 244 ): CameraService::connect call (PID - 1 "com.android.camera2" , camera ID 0 ) for HAL version default and Camera API version 1 08 - 17 16 : 17 : 35.798 E/CameraService( 244 ): CameraService::connect X (PID 1442 ) rejected (invalid camera ID 0 ) 08 - 17 16 : 17 : 35.798 W/CameraBase( 1442 ): An error occurred while connecting to camera 0 : Service not available 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): RuntimeException during CameraAction[OPEN_CAMERA] at CameraState[ 1 ] 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): java.lang.RuntimeException: Fail to connect to camera service 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): at android.hardware.Camera.<init>(Camera.java: 496 ) 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): at android.hardware.Camera.open(Camera.java: 345 ) 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): at com.android.ex.camera2.portability.AndroidCameraAgentImpl$CameraHandler.handleMessage(AndroidCameraAgentImpl.java: 384 ) 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): at android.os.Handler.dispatchMessage(Handler.java: 102 ) 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): at android.os.Looper.loop(Looper.java: 154 ) 08 - 17 16 : 17 : 35.799 E/CAM2PORT_AndCamAgntImp( 1442 ): at android.os.HandlerThread.run(HandlerThread.java: 61 ) 08 - 17 16 : 17 : 35.801 D/CameraHal( 244 ): createInstance( 1041 ): open xml file(/etc/cam_board.xml) success 08 - 17 16 : 17 : 35.801 E/CameraHal( 244 ): cam_board.xml version(v0x0. 0x0 . 0x0 ) != xml parser version(v0x0. 0xf . 0x0 ) |
不使用反射 修改标准Surface 将客户SDK api 封装进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | diff --git a/frameworks/base/core/java/android/view/Surface.java b/frameworks/base/core/java/android/view/Surface.java old mode 100644 new mode 100755 index 22e68a3..78c78d0 --- a/frameworks/base/core/java/android/view/Surface.java +++ b/frameworks/base/core/java/android/view/Surface.java @@ - 31 , 6 + 31 , 11 @@ import java.lang.annotation.RetentionPolicy; import dalvik.system.CloseGuard; + import android.os.SystemProperties; + import java.lang.reflect.Method; + import com.yp.ypcodereader.manager.YpDevicesManager; + import android.view.TextureView; + /** * Handle onto a raw buffer that is being managed by the screen compositor. * @@ -158,8 +163,10 @@ public class Surface implements Parcelable { public Surface() { } + private Surface xhSurface; + /** - * Create Surface from a {@link SurfaceTexture}. + * Create Surface from a {@link SurfaceTex ture}. * * Images drawn to the Surface will be made available to the {@link * SurfaceTexture}, which can attach them to an OpenGL ES texture via {@link @@ -178,8 +185,19 @@ public class Surface implements Parcelable { mName = surfaceTexture.toString(); setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture)); } + if(TextureView.xhSurface_flag) { + GetXSurface(surfaceTexture); + } } + public void GetXSurface(SurfaceTexture surfaceTexture) { + TextureView.xhSurface_flag = false; + xhSurface = new Surface(surfaceTexture); + TextureView.ypDevicesManager.setCodereaderCameraView(xhSurface); + TextureView.ypDevicesManager.openCodereaderCamera(null); + } + + /* called from android_view_Surface_createFromIGraphicBufferProducer() */ private Surface( long nativeObject) { synchronized (mLock) { @@ - 225 , 6 + 243 , 7 @@ public class Surface implements Parcelable { */ public void destroy() { release(); + TextureView.ypDevicesManager.closeCodereaderCamera(); } /** diff --git a/frameworks/base/core/java/android/view/TextureView.java b/frameworks/base/core/java/android/view/TextureView.java old mode 100644 new mode 100755 index 645ab5c..0582f5d --- a/frameworks/base/core/java/android/view/TextureView.java +++ b/frameworks/base/core/java/android/view/TextureView.java @@ -27,6 +27,14 @@ import android.graphics.SurfaceTexture; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; +import android.text.TextUtils; +import android.os.SystemProperties; +import android.app.ActivityManager; +import android.content.ComponentName; + +import com.yp.ypcodereader.manager.YpDevicesManager; + +import java.util.List; /** * <p>A TextureView can be used to display a content stream. Such a content @@ -123,6 +131,12 @@ public class TextureView extends View { private Canvas mCanvas; private int mSaveCount; + public static YpDevicesManager ypDevicesManager; + private String outPackageInfo; + private String CLASS_NAME = "com.gatsby.ypcamerademo2.MainActivity"; + public static boolean xhSurface_flag; + + private final Object[] mNativeWindowLock = new Object[0]; // Set by native code, do not write! private long mNativeWindow; @@ -144,8 +158,27 @@ public class TextureView extends View { */ public TextureView(Context context, AttributeSet attrs) { super (context, attrs); + boolean packageInfo = isForeground(context,CLASS_NAME); + if (packageInfo){ + xhSurface_flag = true ; + ypDevicesManager = new YpDevicesManager().getInstance(); + ypDevicesManager.setContext(context); + } + } + + public static boolean isForeground(Context context, String className) { + if (context == null || TextUtils.isEmpty(className)) + return false ; + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + List<ActivityManager.RunningTaskInfo> list = am.getRunningTasks( 1 ); + if (list != null && list.size() > 0 ) { + ComponentName cpn = list.get( 0 ).topActivity; + if (className.equals(cpn.getClassName())) + return true ; + } + return false ; } - + /** * Creates a new TextureView. * |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】