HarmonyOS多媒体开发:自定义相机与音频播放器实战

一、HarmonyOS多媒体框架概述

HarmonyOS多媒体框架提供了统一的接口来访问设备的多媒体能力,支持跨设备协同工作。框架采用分层架构,从底层的硬件抽象到上层的应用接口,为开发者提供完整的多媒体解决方案。

1.1 多媒体核心组件

相机服务架构

  • CameraKit:相机能力集,提供拍照、录像、预览等核心功能
  • ImageSource:图像数据源,支持多种格式的图像编解码
  • EffectKit:实时特效处理,支持滤镜、美颜等效果

音频服务架构

  • AudioRenderer:音频渲染器,负责音频数据播放
  • AudioCapturer:音频采集器,负责音频数据录制
  • AudioStreamManager:音频流管理,支持多路音频混合

二、自定义相机实战

2.1 相机权限与配置

首先在module.json5中声明必要的权限:

复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "用于自定义相机拍照和录像功能",
        "usedScene": {
          "abilities": ["CameraAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "录制视频时需要采集音频",
        "usedScene": {
          "abilities": ["CameraAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "保存拍摄的照片和视频到相册",
        "usedScene": {
          "abilities": ["CameraAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

2.2 相机初始化与预览

实现完整的相机管理类,处理相机的生命周期和状态管理:

复制代码
import camera from '@ohos.multimedia.camera';
import image from '@ohos.multimedia.image';
import photoAccessHelper from '@ohos.file.photoAccessHelper';

@Component
export class CustomCameraManager {
  private cameraManager: camera.CameraManager | null = null;
  private cameraDevice: camera.CameraDevice | null = null;
  private captureSession: camera.CaptureSession | null = null;
  private previewOutput: camera.PreviewOutput | null = null;
  private photoOutput: camera.PhotoOutput | null = null;
  private videoOutput: camera.VideoOutput | null = null;
  private isCameraReady: boolean = false;
  
  // 初始化相机系统
  async initializeCamera(context: common.Context): Promise<void> {
    try {
      this.cameraManager = camera.getCameraManager(context);
      
      // 获取相机列表并选择后置相机
      const cameras = this.cameraManager.getSupportedCameras();
      const backCamera = cameras.find(cam => 
        cam.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );
      
      if (!backCamera) {
        throw new Error('未找到后置摄像头');
      }
      
      // 创建相机设备
      this.cameraDevice = this.cameraManager.createCameraDevice(backCamera);
      
      // 创建捕获会话
      this.captureSession = this.cameraManager.createCaptureSession();
      
      // 配置会话模板
      await this.captureSession.beginConfig();
      
      // 创建预览输出
      this.previewOutput = this.cameraManager.createPreviewOutput(
        this.getPreviewSurface()
      );
      await this.captureSession.addOutput(this.previewOutput);
      
      // 创建照片输出
      this.photoOutput = this.cameraManager.createPhotoOutput(
        this.getPhotoProfile()
      );
      await this.captureSession.addOutput(this.photoOutput);
      
      // 创建视频输出(如果需要录像)
      this.videoOutput = this.cameraManager.createVideoOutput(
        this.getVideoProfile()
      );
      await this.captureSession.addOutput(this.videoOutput);
      
      // 提交配置并启动预览
      await this.captureSession.commitConfig();
      await this.captureSession.start();
      
      this.isCameraReady = true;
      console.info('相机初始化成功');
      
    } catch (error) {
      console.error('相机初始化失败:', error);
      throw error;
    }
  }
  
  // 拍照功能
  async takePhoto(): Promise<string> {
    if (!this.captureSession || !this.photoOutput) {
      throw new Error('相机未就绪');
    }
    
    try {
      const photo = await this.photoOutput.capture();
      
      // 保存照片到相册
      const photoUri = await this.savePhotoToGallery(photo);
      console.info('照片保存成功:', photoUri);
      
      return photoUri;
    } catch (error) {
      console.error('拍照失败:', error);
      throw error;
    }
  }
  
  // 实时滤镜处理
  async applyRealTimeFilter(filterType: string): Promise<void> {
    if (!this.captureSession) return;
    
    try {
      // 使用EffectKit应用实时滤镜
      const effectKit = await import('@ohos.multimedia.effectKit');
      const filter = effectKit.createEffect(filterType);
      
      // 配置滤镜参数
      await filter.configure({
        intensity: 0.8,
        colorAdjustment: this.getFilterParams(filterType)
      });
      
      // 将滤镜应用到预览流
      await this.previewOutput?.addEffect(filter);
      
    } catch (error) {
      console.error('滤镜应用失败:', error);
    }
  }
  
  // 手动相机控制
  async setManualCameraSettings(settings: CameraSettings): Promise<void> {
    if (!this.cameraDevice) return;
    
    try {
      // 设置曝光补偿
      if (settings.exposureCompensation !== undefined) {
        await this.cameraDevice.setExposureCompensation(settings.exposureCompensation);
      }
      
      // 设置ISO
      if (settings.iso !== undefined) {
        await this.cameraDevice.setIso(settings.iso);
      }
      
      // 设置快门速度
      if (settings.shutterSpeed !== undefined) {
        await this.cameraDevice.setShutterSpeed(settings.shutterSpeed);
      }
      
      // 设置对焦模式
      if (settings.focusMode !== undefined) {
        await this.cameraDevice.setFocusMode(settings.focusMode);
      }
      
    } catch (error) {
      console.error('相机设置失败:', error);
    }
  }
}

2.3 相机UI界面实现

创建功能完整的相机用户界面:

复制代码
@Entry
@Component
struct CustomCameraPage {
  @State currentFilter: string = 'normal';
  @State isRecording: boolean = false;
  @State flashMode: string = 'off';
  @State cameraMode: string = 'photo';
  @State previewRatio: number = 16 / 9;
  
  private cameraManager: CustomCameraManager = new CustomCameraManager();
  private cameraContext: common.Context | null = null;
  
  build() {
    Column({ space: 0 }) {
      // 相机预览区域
      Stack({ alignContent: Alignment.Top }) {
        // 相机预览Surface
        CameraPreviewSurface({ manager: this.cameraManager })
          .width('100%')
          .height('80%')
          .backgroundColor('#000000')
        
        // 顶部控制栏
        Row({ space: 20 }) {
          Button('关闭')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('rgba(0,0,0,0.5)')
            .onClick(() => this.exitCamera())
          
          Text(this.cameraMode === 'photo' ? '照片' : '视频')
            .fontSize(18)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
          
          Button(this.flashMode === 'on' ? '闪光灯开' : '闪光灯关')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('rgba(0,0,0,0.5)')
            .onClick(() => this.toggleFlash())
        }
        .width('100%')
        .padding(20)
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .layoutWeight(1)
      
      // 底部控制区域
      Column({ space: 15 }) {
        // 模式选择
        Row({ space: 30 }) {
          Button('照片')
            .selected(this.cameraMode === 'photo')
            .onClick(() => this.switchMode('photo'))
          
          Button('视频')
            .selected(this.cameraMode === 'video')
            .onClick(() => this.switchMode('video'))
        }
        
        // 拍摄按钮
        Row({ space: 40 }) {
          // 滤镜选择
          Scroll() {
            Row({ space: 10 }) {
              ForEach(this.getAvailableFilters(), (filter: string) => {
                Text(this.getFilterName(filter))
                  .fontSize(14)
                  .fontColor(this.currentFilter === filter ? '#007AFF' : '#FFFFFF')
                  .padding(8)
                  .borderRadius(20)
                  .backgroundColor(this.currentFilter === filter ? 
                    'rgba(0,122,255,0.2)' : 'rgba(255,255,255,0.1)')
                  .onClick(() => this.applyFilter(filter))
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          
          // 拍摄按钮
          Circle({ width: 70, height: 70 })
            .fill(this.isRecording ? '#FF3B30' : '#FFFFFF')
            .onClick(() => this.captureAction())
            .margin({ left: 20, right: 20 })
          
          // 相册入口
          Button('相册')
            .fontSize(16)
            .fontColor('#FFFFFF')
        }
        .justifyContent(FlexAlign.SpaceAround)
      }
      .padding(20)
      .backgroundColor('rgba(0,0,0,0.8)')
      .height('20%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
  
  aboutToAppear(): void {
    this.cameraContext = getContext(this) as common.Context;
    this.initializeCamera();
  }
  
  async initializeCamera(): Promise<void> {
    try {
      await this.cameraManager.initializeCamera(this.cameraContext!);
    } catch (error) {
      console.error('相机初始化失败:', error);
    }
  }
  
  async captureAction(): Promise<void> {
    if (this.cameraMode === 'photo') {
      await this.takePhoto();
    } else {
      await this.toggleRecording();
    }
  }
  
  async takePhoto(): Promise<void> {
    try {
      const photoUri = await this.cameraManager.takePhoto();
      // 显示拍摄结果
      this.showCaptureResult(photoUri);
    } catch (error) {
      console.error('拍照失败:', error);
    }
  }
}

三、音频播放器实战

3.1 音频播放器核心实现

创建功能完整的音频播放器,支持多种音频格式和播放模式:

复制代码
import audio from '@ohos.multimedia.audio';
import mediaLibrary from '@ohos.file.mediaLibrary';

@Component
export class AudioPlayerManager {
  private audioRenderer: audio.AudioRenderer | null = null;
  private audioStreamInfo: audio.AudioStreamInfo | null = null;
  private currentState: PlayerState = PlayerState.IDLE;
  private currentPlaylist: AudioItem[] = [];
  private currentIndex: number = 0;
  private playbackSpeed: number = 1.0;
  private equalizer: audio.AudioEffect | null = null;
  
  // 初始化音频渲染器
  async initializeAudioRenderer(options: AudioRendererOptions): Promise<void> {
    try {
      this.audioStreamInfo = {
        samplingRate: options.samplingRate || audio.AudioSamplingRate.SAMPLE_RATE_44100,
        channels: options.channels || audio.AudioChannel.CHANNEL_2,
        sampleFormat: options.sampleFormat || audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
        encodingType: options.encodingType || audio.AudioEncodingType.ENCODING_TYPE_RAW
      };
      
      const audioRendererOptions: audio.AudioRendererOptions = {
        streamInfo: this.audioStreamInfo,
        rendererInfo: {
          content: audio.ContentType.CONTENT_TYPE_MUSIC,
          usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
          rendererFlags: 0
        }
      };
      
      this.audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
      await this.audioRenderer.start();
      
      this.currentState = PlayerState.READY;
      console.info('音频播放器初始化成功');
      
    } catch (error) {
      console.error('音频播放器初始化失败:', error);
      throw error;
    }
  }
  
  // 播放音频
  async playAudio(audioItem: AudioItem): Promise<void> {
    if (!this.audioRenderer) {
      throw new Error('音频播放器未初始化');
    }
    
    try {
      // 停止当前播放
      if (this.currentState === PlayerState.PLAYING) {
        await this.stop();
      }
      
      // 加载音频数据
      const audioData = await this.loadAudioData(audioItem.uri);
      
      // 配置音频效果
      await this.configureAudioEffects(audioItem);
      
      // 开始播放
      await this.audioRenderer.write(audioData);
      this.currentState = PlayerState.PLAYING;
      
      // 设置播放完成回调
      this.audioRenderer.on('endOfStream', () => {
        this.handlePlaybackComplete();
      });
      
    } catch (error) {
      console.error('音频播放失败:', error);
      throw error;
    }
  }
  
  // 音频效果处理
  async configureAudioEffects(audioItem: AudioItem): Promise<void> {
    if (!this.audioRenderer) return;
    
    try {
      // 创建均衡器
      this.equalizer = await audio.createAudioEffect(audio.AudioEffectType.EQUALIZER);
      
      // 配置音效预设
      const preset = this.getEqualizerPreset(audioItem.genre);
      await this.equalizer.setParameter({
        bandLevels: preset.bandLevels,
        preset: preset.presetType
      });
      
      // 应用音效到音频渲染器
      await this.audioRenderer.attachAudioEffect(this.equalizer);
      
    } catch (error) {
      console.error('音效配置失败:', error);
    }
  }
  
  // 变速播放
  async setPlaybackSpeed(speed: number): Promise<void> {
    if (!this.audioRenderer || speed < 0.5 || speed > 2.0) {
      return;
    }
    
    try {
      this.playbackSpeed = speed;
      await this.audioRenderer.setPlaybackSpeed(speed);
    } catch (error) {
      console.error('设置播放速度失败:', error);
    }
  }
  
  // 睡眠定时器
  async setSleepTimer(minutes: number): Promise<void> {
    setTimeout(async () => {
      if (this.currentState === PlayerState.PLAYING) {
        await this.pause();
        console.info('睡眠定时器:播放已停止');
      }
    }, minutes * 60 * 1000);
  }
}

3.2 音频播放器UI界面

创建美观易用的音频播放界面:

复制代码
@Entry
@Component
struct AudioPlayerPage {
  @State currentSong: AudioItem | null = null;
  @State isPlaying: boolean = false;
  @State currentTime: number = 0;
  @State totalTime: number = 0;
  @State playbackRate: number = 1.0;
  @State equalizerPreset: string = 'normal';
  @State showPlaylist: boolean = false;
  
  private audioManager: AudioPlayerManager = new AudioPlayerManager();
  private progressTimer: number = 0;
  
  build() {
    Column({ space: 0 }) {
      // 专辑封面和歌曲信息
      Column({ space: 20 }) {
        Image(this.currentSong?.coverUri || $r('app.media.default_cover'))
          .width(280)
          .height(280)
          .borderRadius(20)
          .objectFit(ImageFit.Contain)
        
        Column({ space: 10 }) {
          Text(this.currentSong?.title || '未选择歌曲')
            .fontSize(24)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
          
          Text(this.currentSong?.artist || '未知艺术家')
            .fontSize(16)
            .fontColor('#CCCCCC')
        }
      }
      .layoutWeight(1)
      .padding(40)
      
      // 播放进度条
      Column({ space: 10 }) {
        Slider({
          value: this.currentTime,
          min: 0,
          max: this.totalTime,
          step: 1,
          style: SliderStyle.OutSet
        })
        .width('90%')
        .onChange((value: number) => {
          this.seekTo(value);
        })
        
        Row({ space: 0 }) {
          Text(this.formatTime(this.currentTime))
            .fontSize(12)
            .fontColor('#CCCCCC')
            .layoutWeight(1)
            .textAlign(TextAlign.Start)
          
          Text(this.formatTime(this.totalTime))
            .fontSize(12)
            .fontColor('#CCCCCC')
            .layoutWeight(1)
            .textAlign(TextAlign.End)
        }
        .width('90%')
      }
      .padding(20)
      
      // 播放控制区域
      Column({ space: 25 }) {
        // 音效控制
        Row({ space: 15 }) {
          Button('变速')
            .fontSize(14)
            .fontColor(this.playbackRate !== 1.0 ? '#007AFF' : '#FFFFFF')
            .onClick(() => this.showSpeedOptions())
          
          Button('音效')
            .fontSize(14)
            .fontColor(this.equalizerPreset !== 'normal' ? '#007AFF' : '#FFFFFF')
            .onClick(() => this.showEqualizerOptions())
          
          Button('定时')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .onClick(() => this.showSleepTimer())
        }
        
        // 主要控制按钮
        Row({ space: 40 }) {
          Button('上一首')
            .fontSize(16)
            .onClick(() => this.previousSong())
          
          Button(this.isPlaying ? '暂停' : '播放')
            .fontSize(20)
            .fontColor('#000000')
            .backgroundColor('#FFFFFF')
            .borderRadius(30)
            .width(60)
            .height(60)
            .onClick(() => this.togglePlayback())
          
          Button('下一首')
            .fontSize(16)
            .onClick(() => this.nextSong())
        }
        
        // 音量和其他控制
        Row({ space: 30 }) {
          Button('播放列表')
            .fontSize(14)
            .onClick(() => this.togglePlaylist())
          
          Button('随机播放')
            .fontSize(14)
            .onClick(() => this.toggleShuffle())
          
          Button('循环模式')
            .fontSize(14)
            .onClick(() => this.toggleLoopMode())
        }
      }
      .padding(30)
      .backgroundColor('rgba(0,0,0,0.8)')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
}

四、多设备音频接力实战

4.1 分布式音频管理

实现跨设备音频接力功能,让用户在不同设备间无缝切换音频播放:

复制代码
import distributedAudio from '@ohos.multimedia.distributedAudio';
import deviceManager from '@ohos.distributedDeviceManager';

@Component
export class DistributedAudioManager {
  private audioDeviceManager: distributedAudio.AudioDeviceManager | null = null;
  private currentDeviceId: string = '';
  private availableDevices: distributedAudio.AudioDevice[] = [];
  
  // 初始化分布式音频
  async initializeDistributedAudio(): Promise<void> {
    try {
      this.audioDeviceManager = distributedAudio.createAudioDeviceManager();
      
      // 监听设备变化
      this.audioDeviceManager.on('deviceChange', (devices) => {
        this.handleDeviceChange(devices);
      });
      
      // 发现可用音频设备
      await this.discoverAudioDevices();
      
    } catch (error) {
      console.error('分布式音频初始化失败:', error);
    }
  }
  
  // 发现可用音频设备
  async discoverAudioDevices(): Promise<void> {
    if (!this.audioDeviceManager) return;
    
    try {
      const devices = await this.audioDeviceManager.getAvailableDevices();
      this.availableDevices = devices.filter(device => 
        device.deviceType !== distributedAudio.DeviceType.UNKNOWN
      );
      
      console.info(`发现${this.availableDevices.length}个可用音频设备`);
    } catch (error) {
      console.error('设备发现失败:', error);
    }
  }
  
  // 切换到其他设备播放
  async switchPlaybackDevice(deviceId: string, audioItem: AudioItem): Promise<void> {
    if (!this.audioDeviceManager) {
      throw new Error('分布式音频未初始化');
    }
    
    try {
      // 暂停当前设备播放
      await this.pauseCurrentPlayback();
      
      // 切换到目标设备
      await this.audioDeviceManager.switchOutputDevice(deviceId);
      
      // 在新设备上恢复播放
      await this.resumePlaybackOnDevice(deviceId, audioItem);
      
      this.currentDeviceId = deviceId;
      console.info(`音频已切换到设备: ${deviceId}`);
      
    } catch (error) {
      console.error('设备切换失败:', error);
      throw error;
    }
  }
  
  // 多设备同步播放
  async startMultiDevicePlayback(devices: string[], audioItem: AudioItem): Promise<void> {
    if (!this.audioDeviceManager) return;
    
    try {
      // 创建同步播放组
      const syncGroup = await this.audioDeviceManager.createSyncGroup(devices);
      
      // 配置同步参数
      await syncGroup.configure({
        syncTolerance: 50, // 50ms同步容差
        masterDevice: devices[0] // 主设备
      });
      
      // 在所有设备上开始同步播放
      await Promise.all(
        devices.map(deviceId => 
          this.startPlaybackOnDevice(deviceId, audioItem)
        )
      );
      
      console.info(`在${devices.length}个设备上开始同步播放`);
      
    } catch (error) {
      console.error('多设备同步播放失败:', error);
    }
  }
}

4.2 设备选择界面

创建设备选择界面,让用户可以轻松管理多设备音频:

复制代码
@Component
struct DeviceSelectorDialog {
  @Link selectedDevice: string;
  @Link availableDevices: distributedAudio.AudioDevice[];
  
  build() {
    Column({ space: 20 }) {
      Text('选择播放设备')
        .fontSize(20)
        .fontColor('#000000')
        .fontWeight(FontWeight.Bold)
      
      List({ space: 10 }) {
        ForEach(this.availableDevices, (device: distributedAudio.AudioDevice) => {
          ListItem() {
            Row({ space: 15 }) {
              Image(this.getDeviceIcon(device.deviceType))
                .width(30)
                .height(30)
              
              Column({ space: 5 }) {
                Text(device.deviceName)
                  .fontSize(16)
                  .fontColor('#000000')
                  .textAlign(TextAlign.Start)
                
                Text(this.getDeviceStatus(device))
                  .fontSize(12)
                  .fontColor('#666666')
              }
              .layoutWeight(1)
              
              if (device.deviceId === this.selectedDevice) {
                Image($r('app.media.ic_selected'))
                  .width(20)
                  .height(20)
              }
            }
            .padding(15)
            .backgroundColor(device.deviceId === this.selectedDevice ? 
              '#E6F2FF' : '#FFFFFF')
            .borderRadius(10)
          }
          .onClick(() => {
            this.selectedDevice = device.deviceId;
          })
        })
      }
      .layoutWeight(1)
      
      Button('确认切换')
        .width('80%')
        .height(40)
        .fontSize(16)
        .onClick(() => {
          // 处理设备切换
          this.confirmSelection();
        })
    }
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(20)
  }
}

五、性能优化与最佳实践

5.1 内存管理优化

复制代码
@Component
export class MediaMemoryManager {
  private static readonly MAX_CACHE_SIZE = 100 * 1024 * 1024; // 100MB
  private currentCacheSize: number = 0;
  private mediaCache: Map<string, ArrayBuffer> = new Map();
  
  // 智能缓存管理
  async cacheMediaData(mediaUri: string, data: ArrayBuffer): Promise<void> {
    const dataSize = data.byteLength;
    
    // 检查缓存限制
    if (this.currentCacheSize + dataSize > MediaMemoryManager.MAX_CACHE_SIZE) {
      await this.cleanupCache();
    }
    
    this.mediaCache.set(mediaUri, data);
    this.currentCacheSize += dataSize;
  }
  
  // 内存压力处理
  @WatchSystemMemory
  async onMemoryPressure(level: MemoryPressureLevel): Promise<void> {
    switch (level) {
      case MemoryPressureLevel.CRITICAL:
        await this.clearAllCache();
        break;
      case MemoryPressureLevel.HIGH:
        await this.cleanupCache(0.5); // 清理50%缓存
        break;
      case MemoryPressureLevel.MEDIUM:
        await this.cleanupCache(0.3); // 清理30%缓存
        break;
    }
  }
  
  // 媒体资源预加载
  async preloadMediaResources(resources: string[]): Promise<void> {
    const preloadPromises = resources.map(async resource => {
      if (!this.mediaCache.has(resource)) {
        const data = await this.loadMediaData(resource);
        await this.cacheMediaData(resource, data);
      }
    });
    
    await Promise.all(preloadPromises);
  }
}

总结

本文通过完整的实战案例展示了HarmonyOS多媒体开发的核心技术,包括自定义相机实现、音频播放器开发以及多设备音频接力功能。关键要点包括:

  1. 相机开发:掌握相机API的完整使用流程,包括权限管理、预览配置、拍照录像和实时特效处理
  2. 音频处理:实现高质量的音频播放器,支持音效处理、变速播放和睡眠定时等高级功能
  3. 分布式能力:利用HarmonyOS的分布式特性实现多设备音频接力,提供无缝的用户体验
  4. 性能优化:通过内存管理和资源预加载确保多媒体应用的流畅运行
相关推荐
逻极6 小时前
HarmonyOS 5 鸿蒙应用签名机制与安全开发实战指南
harmonyos
黄卷青灯779 小时前
标定参数从相机模组读出来
数码相机·相机内参
黄卷青灯779 小时前
标定系数为什么会存储在相机模组里面,在标定的时候,算法是在割草机的X3板上运行的啊?
数码相机·算法·相机内参
黄卷青灯7711 小时前
相机模组,模组是什么意思?
数码相机·相机模组
zhuweisky12 小时前
实现一个纯血鸿蒙版(HarmonyOS)的聊天Demo,并可与其它PC、手机端互通!
harmonyos·im·聊天软件
多测师_王sir12 小时前
鸿蒙hdc命令【杭州多测师】
华为·harmonyos
一点七加一13 小时前
Harmony鸿蒙开发0基础入门到精通Day01--JavaScript篇
开发语言·javascript·华为·typescript·ecmascript·harmonyos
那年窗外下的雪.14 小时前
鸿蒙ArkUI布局与样式进阶(十二)——自定义TabBar + class类机制全解析(含手机商城底部导航案例)
开发语言·前端·javascript·华为·智能手机·harmonyos·arkui
格林威14 小时前
近红外工业相机的简单介绍和场景应用
人工智能·深度学习·数码相机·计算机视觉·视觉检测·制造·工业相机