หลังจากที่คุณขอและได้รับสิทธิ์ที่จำเป็นแล้ว แอปจะเข้าถึงฮาร์ดแวร์ในแว่นตาที่มีระบบเสียงหรือแว่นตาที่มีจอแสดงผลได้ กุญแจสำคัญในการเข้าถึงฮาร์ดแวร์ของแว่นตา (แทนฮาร์ดแวร์ของโทรศัพท์) คือการใช้บริบทที่คาดการณ์ไว้
การรับบริบทที่คาดการณ์ไว้มี 2 วิธีหลักๆ ขึ้นอยู่กับตำแหน่งที่โค้ดทำงาน ดังนี้
รับบริบทที่คาดการณ์ไว้หากโค้ดของคุณทำงานในกิจกรรมที่คาดการณ์ไว้
หากโค้ดของแอปทำงานจากภายในกิจกรรมที่ฉายภาพ บริบทกิจกรรมของแอปเองจะเป็นบริบทที่ฉายภาพอยู่แล้ว ในกรณีนี้ การเรียกใช้ที่ทำภายในกิจกรรมนั้นจะเข้าถึงฮาร์ดแวร์ของแว่นตาได้อยู่แล้ว
รับบริบทที่คาดการณ์ไว้สำหรับโค้ดที่ทำงานในคอมโพเนนต์แอปโทรศัพท์
หากส่วนหนึ่งของแอปที่อยู่นอกกิจกรรมที่ฉายภาพ (เช่น กิจกรรมหรือบริการของโทรศัพท์) จำเป็นต้องเข้าถึงฮาร์ดแวร์ของแว่นตา ส่วนนั้นจะต้องได้รับบริบทที่ฉายภาพอย่างชัดแจ้ง โดยใช้วิธีการ createProjectedDeviceContext ดังนี้
@RequiresApi(Build.VERSION_CODES.BAKLAVA) @OptIn(ExperimentalProjectedApi::class, ExperimentalCoroutinesApi::class) private fun monitorProjectedConnectivity(activity: ComponentActivity) { activity.lifecycleScope.launch { // Before creating a projected context, check to see if the projected device is connected. // While this method returns true, the projected context remains valid. ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext) .collectLatest { isConnected -> if (isConnected) { // From a phone Activity or Service, get a context for the audio and display glasses. // Re-initialize on reconnect: Obtain another context instance. val projectedContext = try { ProjectedContext.createProjectedDeviceContext(activity) } catch (e: IllegalStateException) { Log.e(TAG, "Failed to create projected context", e) return@collectLatest } // Use the projectedContext to initialize system services (e.g., CameraManager). Log.i(TAG, "Projected device connected. Initializing hardware...") } else { // The projected context is destroyed when the device disconnects. // Clean up on disconnect: Listen for 'false' and release resources. Log.i(TAG, "Projected device disconnected. Cleaning up hardware resources...") } } } }
ตรวจสอบความถูกต้อง
รวมการเรียก createProjectedDeviceContext ไว้ใน ProjectedContext.isProjectedDeviceConnected แม้ว่าเมธอดนี้จะแสดงผล true แต่บริบทที่คาดการณ์ไว้จะยังคงใช้ได้กับอุปกรณ์ที่เชื่อมต่อ และกิจกรรมหรือบริการของแอปโทรศัพท์ (เช่น CameraManager) จะเข้าถึงฮาร์ดแวร์ของแว่นตาได้
ล้างข้อมูลเมื่อยกเลิกการเชื่อมต่อ
บริบทที่ฉายจะเชื่อมโยงกับวงจรของอุปกรณ์ที่เชื่อมต่อ ดังนั้นระบบจะทำลายบริบทดังกล่าวเมื่ออุปกรณ์ยกเลิกการเชื่อมต่อ เมื่ออุปกรณ์ยกเลิกการเชื่อมต่อ
ProjectedContext.isProjectedDeviceConnected จะแสดงผลเป็น false แอปของคุณควรคอยฟังการเปลี่ยนแปลงนี้และล้างบริการของระบบ (เช่น CameraManager) หรือทรัพยากรที่แอปสร้างขึ้นโดยใช้บริบทที่ฉายภาพนั้น
เริ่มต้นใหม่เมื่อเชื่อมต่ออีกครั้ง
เมื่อแว่นตาเชื่อมต่ออีกครั้ง แอปจะรับอินสแตนซ์บริบทที่ฉายอีกรายการได้โดยใช้ createProjectedDeviceContext จากนั้นเริ่มต้นบริการหรือทรัพยากรระบบอีกครั้งโดยใช้บริบทที่ฉายใหม่
บันทึกเสียงด้วยไมโครโฟนของแว่นตา
คุณสามารถบันทึกเสียงจากแว่นตาโดยใช้โปรไฟล์แฮนด์ฟรี (HFP) ของบลูทูธได้ ในโหมดนี้ แว่นตาจะเชื่อมต่อกับโทรศัพท์โดยใช้โปรไฟล์ Headset และ Advanced Audio Distribution Profile (A2DP) มาตรฐาน ซึ่งทำงานเหมือนกับอุปกรณ์ต่อพ่วงบลูทูธทั่วไป หากแอปของคุณออกแบบมาสำหรับการบันทึกเสียงผ่านบลูทูธมาตรฐานอยู่แล้ว คุณสามารถใช้วิธีนี้เพื่อบันทึกเสียงจากแว่นตาได้โดยไม่ต้องพัฒนาเพิ่มเติม
บันทึกเสียงโดยใช้บลูทูธ HFP
หากต้องการบันทึกเสียงโดยใช้ Bluetooth HFP ให้ขอสิทธิ์รันไทม์ที่จำเป็นก่อน แล้วจึงบันทึกเสียงโดยใช้ AudioManager API ตามที่อธิบายไว้ในส่วนต่อไปนี้
ขอสิทธิ์
เช่นเดียวกับอุปกรณ์เสียงบลูทูธมาตรฐาน สิทธิ์ RECORD_AUDIO, BLUETOOTH_CONNECT และสิทธิ์อื่นๆ ที่เกี่ยวข้องจะควบคุมโดยโทรศัพท์ ไม่ใช่อุปกรณ์ที่เชื่อมต่อ (เช่น แว่นตาที่มีระบบเสียง หรือแว่นตาที่มีจอแสดงผล)
ทำตามขั้นตอนต่อไปนี้เพื่อขอสิทธิ์
ประกาศสิทธิ์ต่อไปนี้ในไฟล์ Manifest ของแอป
ขอทั้งสิทธิ์
RECORD_AUDIOและBLUETOOTH_CONNECTขณะรันไทม์โดยใช้ขั้นตอนการขอสิทธิ์ Android มาตรฐาน
ใช้ AudioManager เพื่อกำหนดเส้นทางเสียง
หลังจากที่ผู้ใช้ให้สิทธิ์รันไทม์ที่จำเป็นแก่แอปแล้ว ให้ใช้ API AudioManager เพื่อตั้งค่าอุปกรณ์สื่อสารเป็น TYPE_BLUETOOTH_SCO เพื่อกำหนดเส้นทางเสียงผ่าน Bluetooth HFP ซึ่งจะสั่งให้ระบบดึงข้อมูลเสียงจากอุปกรณ์ต่อพ่วงบลูทูธ
val audioManager = context.getSystemService(AudioManager::class.java) ?: return val hfpDevice = audioManager.availableCommunicationDevices .firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } ?: return // HFP input is limited to 16 kHz, with mono or stereo channel configurations. val sampleRate = 16_000 val channelMask = AudioFormat.CHANNEL_IN_MONO val encoding = AudioFormat.ENCODING_PCM_16BIT // Use a small multiple of the minimum buffer size to keep chunks short for low latency. val minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelMask, encoding) if (minBufferSize <= 0) return val audioRecord = AudioRecord.Builder() .setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION) .setAudioFormat( AudioFormat.Builder() .setSampleRate(sampleRate) .setChannelMask(channelMask) .setEncoding(encoding) .build() ) .setBufferSizeInBytes(minBufferSize * 2) .build() try { // Route voice communication audio through the Bluetooth device. if (!audioManager.setCommunicationDevice(hfpDevice)) return audioRecord.startRecording() // Read audio with audioRecord.read() on a background thread. } finally { // Stop and release when done. if (audioRecord.recordingState == AudioRecord.RECORDSTATE_RECORDING) audioRecord.stop() audioRecord.release() audioManager.clearCommunicationDevice() }
จับภาพด้วยกล้องของแว่นตา
หากต้องการจับภาพด้วยกล้องของแว่นตา ให้ตั้งค่าและเชื่อมโยง กรณีการใช้งาน ImageCapture ของ CameraX กับกล้องของแว่นตาโดยใช้บริบทที่ถูกต้องสำหรับแอปของคุณ ดังนี้
private fun setUpGlassesCamera(activity: ComponentActivity) { activity.lifecycleScope.launch { // Before creating a projected context, check to see if the projected device is connected. ProjectedContext.isProjectedDeviceConnected(activity, coroutineContext) .collectLatest { isConnected -> if (isConnected) { // 1. Get the CameraProvider using the projected context. // When using the projected context, DEFAULT_BACK_CAMERA maps to the audio and display glasses' camera. val projectedContext = try { ProjectedContext.createProjectedDeviceContext(activity) } catch (e: IllegalStateException) { Log.e(TAG, "Projected context could not be created", e) return@collectLatest } val cameraProviderFuture = ProcessCameraProvider.getInstance(projectedContext) cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA // 2. Check for the presence of a camera. if (!cameraProvider.hasCamera(cameraSelector)) { Log.w(TAG, "The selected camera is not available.") return@addListener } // 3. Query supported streaming resolutions dynamically using Camera2 Interop. val cameraInfo = cameraProvider.getCameraInfo(cameraSelector) val map = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP ) val supportedResolutions = map?.getOutputSizes(ImageFormat.JPEG)?.toList() ?: emptyList() Log.i(TAG, "Supported resolutions for JPEG: ${supportedResolutions.joinToString()}") // 4. Dynamically select the best target resolution from the queried list. val preferredResolution = Size(1920, 1080) val dynamicTargetResolution = when { // Case A: The preferred resolution is explicitly supported by the glasses. supportedResolutions.contains(preferredResolution) -> preferredResolution // Case B: Find the highest supported 16:9 resolution as a fallback. else -> { val targetAspectRatio = 16.0 / 9.0 val tolerance = 0.01 supportedResolutions .filter { abs((it.width.toDouble() / it.height) - targetAspectRatio) < tolerance } .maxByOrNull { it.width * it.height } // Case C: Absolute fallback if no 16:9 resolutions are found. ?: supportedResolutions.firstOrNull() ?: preferredResolution } } Log.i(TAG, "Selected target resolution: $dynamicTargetResolution") val resolutionStrategy = ResolutionStrategy( dynamicTargetResolution, ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER ) val resolutionSelector = ResolutionSelector.Builder() .setResolutionStrategy(resolutionStrategy) .build() // 5. Initialize the ImageCapture use case with options. val imageCapture = ImageCapture.Builder() // Optional: Configure resolution, format, etc. .setResolutionSelector(resolutionSelector) .build() try { // Unbind use cases before rebinding. cameraProvider.unbindAll() // Bind use cases to camera using the Activity as the LifecycleOwner. cameraProvider.bindToLifecycle( activity, cameraSelector, imageCapture ) } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(activity)) } } } }
ประเด็นสำคัญเกี่ยวกับโค้ด
- รับอินสแตนซ์ของ
ProcessCameraProviderโดยใช้บริบทอุปกรณ์ที่ฉายภาพ - ภายในขอบเขตของบริบทที่ฉาย กล้องหลักที่หันออกด้านนอกของแว่นตาจะแมปกับ
DEFAULT_BACK_CAMERAเมื่อเลือกกล้อง - การตรวจสอบก่อนการเชื่อมโยงจะใช้
cameraProvider.hasCamera(cameraSelector)เพื่อยืนยันว่ากล้องที่เลือกพร้อมใช้งานในอุปกรณ์ก่อนดำเนินการต่อ - ใช้ Camera2 Interop กับ
Camera2CameraInfoเพื่ออ่านCameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAPที่สำคัญ ซึ่งอาจเป็นประโยชน์สำหรับการตรวจสอบขั้นสูงเกี่ยวกับความละเอียดที่รองรับ ResolutionSelectorที่กำหนดเองสร้างขึ้นเพื่อควบคุมความละเอียดของรูปภาพเอาต์พุตสำหรับImageCaptureอย่างแม่นยำ- สร้างกรณีการใช้งาน
ImageCaptureที่กำหนดค่าด้วยResolutionSelectorที่กำหนดเอง - เชื่อมโยงกรณีการใช้งาน
ImageCaptureกับวงจรของกิจกรรม ซึ่งจะจัดการการเปิดและปิดกล้องโดยอัตโนมัติตามสถานะของกิจกรรม (เช่น การหยุดกล้องเมื่อกิจกรรมหยุดชั่วคราว) - การกำหนดค่าสำหรับฮาร์ดแวร์สตรีมเดียว: ไปป์ไลน์กล้องของแว่นตาถูกจำกัดให้สตรีมที่ใช้งานอยู่ได้ครั้งละ 1 รายการเท่านั้น เมื่อเชื่อมโยง
ImageCaptureเพียงอย่างเดียว CameraX จะแนบสตรีมที่ทำซ้ำภายใน (MeteringRepeating) สำหรับการโฟกัสและการวัดแสงโดยค่าเริ่มต้น ซึ่งทำให้ไปป์ไลน์สตรีมเดียวหยุดชะงัก หากต้องการเชื่อมโยงImageCaptureด้วยตัวเองโดยไม่มีสตรีมตัวอย่าง ให้ปิดใช้สตรีมที่บังคับให้ทำซ้ำโดยตั้งค่าsetRepeatingStreamForced(false)ในCameraXConfig.Builder(เช่น โดยการใช้CameraXConfig.ProviderในคลาสApplicationหรือกำหนดค่าProcessCameraProviderก่อนการเริ่มต้น)
หลังจากตั้งค่ากล้องของแว่นตาแล้ว คุณจะจับภาพได้ด้วยคลาส ImageCapture ของ CameraX โปรดดูเอกสารประกอบของ CameraX เพื่อดูข้อมูลเกี่ยวกับการใช้ takePicture เพื่อจับภาพ
จับภาพสตรีมด้วยกล้องของแว่นตา
หากต้องการจับภาพสตรีมแทนรูปภาพ ให้แทนที่คอมโพเนนต์ ImageCapture จากตัวอย่างการจับภาพด้วยคอมโพเนนต์ VideoCapture หรือ ImageAnalysis แล้วเปลี่ยนตรรกะการจับภาพ
ดูข้อมูลเพิ่มเติมเกี่ยวกับ API เหล่านี้และวิธีใช้ได้ในเอกสารประกอบเกี่ยวกับการบันทึกวิดีโอด้วย CameraX และการวิเคราะห์รูปภาพด้วย CameraX
เลือกความละเอียดและอัตราเฟรม
คุณควรค้นหาอุปกรณ์เพื่อดูความละเอียดที่รองรับและอัตราเฟรมสูงสุดที่สอดคล้องกัน ตารางต่อไปนี้แสดงตัวอย่างค่าที่คุณอาจเห็น โดยขึ้นอยู่กับกรณีการใช้งานของแอป หากต้องการประหยัดพลังงานแบตเตอรี่และลดความร้อนของแว่นตา ให้เลือกความละเอียดและอัตราเฟรมต่ำสุดที่เหมาะกับกรณีการใช้งานของคุณ
| กรณีการใช้งาน | ความละเอียด | อัตราเฟรม |
|---|---|---|
| การจดจำข้อความ (OCR) หรือการตรวจจับสภาพแวดล้อม | 1920 x 1080 | 1 FPS |
| สตรีมมิงวิดีโอ AI (ผู้ช่วยแบบหลายรูปแบบ) | 1024 x 768 | 1 FPS |
| การแปลและการระบุตำแหน่งด้วยภาพ (VPS) | 640 x 360 หรือ 640 x 480 | 10 FPS |
| วิดีโอคอล | 640 x 360 หรือ 640 x 480 | 24 FPS |
ค้นหาอัตราเฟรมสูงสุดสำหรับความละเอียดแต่ละรายการ
CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES จะรายงานขอบเขตอัตราเฟรมโดยรวมของอุปกรณ์ ซึ่งอาจสูงกว่าอัตราการสตรีมที่ความละเอียดหนึ่งๆ รองรับ หากต้องการดูค่าสูงสุดจริงสำหรับความละเอียดแต่ละรายการในแว่นตาที่มีจอแสดงผลและเสียง ให้อ่านระยะเวลาเฟรมขั้นต่ำและแปลงเป็น FPS ด้วย StreamConfigurationMap.getOutputMinFrameDuration(format, size):
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class) fun getMaxFpsBySize(cameraInfo: CameraInfo, format: Int): Map<Size, Int> { val map = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP ) ?: return emptyMap() return map.getOutputSizes(format).associateWith { size -> val minFrameDurationNs = map.getOutputMinFrameDuration(format, size) if (minFrameDurationNs > 0) (1_000_000_000L / minFrameDurationNs).toInt() else 0 } }
จากนั้นใช้ค่านั้นเพื่อคำนวณขอบเขต minFps และ maxFps สำหรับช่วงอัตราเฟรมเป้าหมาย จำกัดขอบเขตล่าง (minFps) ที่อัตราเฟรมสตรีมมิงสูงสุดของความละเอียดที่คำนวณจาก getOutputMinFrameDuration และตั้งค่าขอบเขตบนที่สูงขึ้น (maxFps) เป็นกรอบเวลาการไล่ตาม:
val maxFpsBySize = getMaxFpsBySize(cameraInfo, ImageFormat.YUV_420_888) val maxSupportedFps = maxFpsBySize[Size(640, 480)] ?: return ImageAnalysis.Builder().build() // minFps is the target streaming frame rate and should be at most maxSupportedFps. val minFps = minOf(desiredFps, maxSupportedFps) // Set the upper bound to 2x minFps so the stream can catch up if frames are delayed. val maxFps = minFps * 2
ประเด็นสำคัญเกี่ยวกับโค้ด
- ใช้รูปแบบที่ตรงกับกรณีการใช้งานของคุณ สำหรับ
ImageAnalysisคือYUV_420_888 - สำหรับ
VideoCaptureให้ใช้โอเวอร์โหลดClassแทน ดังนี้getOutputSizes(MediaRecorder::class.java)และgetOutputMinFrameDuration(MediaRecorder::class.java, size) - ระบุช่วงที่
maxFpsมากกว่าminFps(เช่นminFps*2) แทนที่จะตั้งค่าขอบเขตทั้ง 2 เป็นค่าเดียวกัน เช่นRange(minFps, minFps)ขอบเขตล่าง (minFps) จะกำหนดอัตราเฟรมสตรีมมิงเป้าหมายและควรมีค่าไม่เกินค่าที่คำนวณจากgetOutputMinFrameDurationส่วนขอบเขตบน (maxFps) จะทำหน้าที่เป็นกรอบเวลาไล่ตามสำหรับเฟรมที่ล่าช้าเพื่อป้องกันเวลาในการตอบสนองที่ต่อเนื่อง เช่น [1,2] หรือ [1,3] สำหรับสตรีม 1 FPS และ [24,30] หรือ [24,48] สำหรับสตรีม 24 FPS
กำหนดค่าอัตราเฟรม
วิธีกำหนดค่าช่วงอัตราเฟรมตามกรณีการใช้งาน CameraX มีดังนี้
// Configure Resolution Strategy for the stream val resolutionStrategy = ResolutionStrategy( size, ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER ) val resolutionSelector = ResolutionSelector.Builder() .setResolutionStrategy(resolutionStrategy) .build() // Create and return the ImageAnalysis builder with proper frame rate ranges // ImageAnalysis: OCR, AI streaming, and VPS val analysisBuilder = ImageAnalysis.Builder() .setResolutionSelector(resolutionSelector) Camera2Interop.Extender(analysisBuilder) .setCaptureRequestOption( CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(minFps, maxFps) ) return analysisBuilder.build()
ตัวอย่างนี้แสดงการกำหนดค่าสำหรับทั้ง VideoCapture และ ImageAnalysis แต่คุณสามารถใช้ Use Case ใดก็ได้ที่แอปรองรับ
ทำความเข้าใจลักษณะการทำงานของสตรีม
โปรดคำนึงถึงลักษณะการทำงานของระบบต่อไปนี้ขณะทำงานกับสตรีมการจับภาพ
- ระบบจะปฏิเสธความละเอียดที่ไม่รองรับ: หากคุณขอขนาดที่ไม่อยู่ในรายการ Camera2 จะปฏิเสธการกำหนดค่าก่อนที่สตรีมจะเปิดขึ้น
- การขออัตราเฟรมที่สูงกว่าค่าสูงสุด: หากคุณขออัตราเฟรมขั้นต่ำที่สูงกว่าค่าสูงสุดที่รองรับสำหรับความละเอียดของคุณ เซสชันจะยังคงเปิดอยู่ แต่อัตราเฟรมเป้าหมายจากแว่นตาจะถูกจำกัดไว้ที่ค่าสูงสุดที่รองรับ ในขณะที่ขอบเขตบนจะยังคงทำหน้าที่เป็นขีดจำกัดอัตราการไล่ตาม เช่น การขอ [30,60] ที่ 640 x 480 จะจำกัดอัตราเฟรมเป้าหมายไว้ที่ 24 FPS (ทำงานเหมือน [24,60])
- การควบคุมปริมาณความร้อน: เมื่อแว่นตาเริ่มร้อน อัตราเฟรมจะลดลงแต่ความละเอียดยังคงเท่าเดิม สร้างแอปให้จัดการอัตราเฟรมที่ต่ำหรือไม่สม่ำเสมอได้ เช่น โดยการตรวจสอบการประทับเวลาของเฟรมแทนที่จะถือว่าอัตราเฟรมคงที่
เข้าถึงฮาร์ดแวร์ของโทรศัพท์จากกิจกรรมที่ฉายภาพ
กิจกรรมที่ฉายยังเข้าถึงฮาร์ดแวร์ของโทรศัพท์ (เช่น กล้องหรือไมโครโฟน) ได้โดยใช้ createHostDeviceContext(context) เพื่อรับบริบทของอุปกรณ์โฮสต์ (โทรศัพท์) ดังนี้
@OptIn(ExperimentalProjectedApi::class) private fun getPhoneContext(activity: ComponentActivity): Context? { return try { // From a projected Activity, get a context for the phone. ProjectedContext.createHostDeviceContext(activity) } catch (e: IllegalStateException) { Log.e(TAG, "Failed to create host device context", e) null } }
เมื่อเข้าถึงฮาร์ดแวร์หรือทรัพยากรที่เฉพาะเจาะจงสำหรับอุปกรณ์โฮสต์ (โทรศัพท์) ในแอปแบบไฮบริด (แอปที่มีทั้งประสบการณ์การใช้งานบนอุปกรณ์เคลื่อนที่และแว่นตา) คุณต้องเลือกบริบทที่ถูกต้องอย่างชัดเจนเพื่อให้แอปเข้าถึงฮาร์ดแวร์ที่ถูกต้องได้ โดยทำดังนี้
- ใช้บริบท
Activityจากโทรศัพท์ActivityหรือProjectedContext.createHostDeviceContextเพื่อรับบริบทของโทรศัพท์ - อย่าใช้
getApplicationContextเนื่องจากบริบทของแอปพลิเคชันอาจแสดงผลบริบทของแว่นตาอย่างไม่ถูกต้องหากกิจกรรมที่ฉายภาพเป็นคอมโพเนนต์ที่เปิดล่าสุด