DEV Community

Cover image for HarmonyOS Next Best Practices for Ringing and Vibration in Call Pages
kouwei qing
kouwei qing

Posted on • Edited on

HarmonyOS Next Best Practices for Ringing and Vibration in Call Pages

HarmonyOS Next Best Practices for Ringing and Vibration in Call Pages

1. Background Introduction

In developing audio-video calling modules, call ringing is typically implemented on both outgoing and incoming call pages, similar to WeChat's video call interface. The called party often experiences vibration alongside ringing. On Android, MediaPlayer loops MP3 ringtones from the raw directory, and Vibrator achieves vibration effects. This article introduces how to implement ringing and vibration in HarmonyOS Next.

2. Ringing Implementation

For package size considerations, we usually place a short ringtone in the app and repeat it for continuous ringing. Call pages often limit the maximum duration to one minute, so the ringtone can play for up to one minute.

We place the ringtone file in rawfile and read it via resourceManager. HarmonyOS provides two ways to play audio:

  • SoundPool: Enables low-latency short sound playback, suitable for急促 brief sound effects (e.g., camera shutter, system notifications). It supports audio resources under 1MB; files over 1MB are truncated to 1MB.
  • AVPlayer: Achieves end-to-end playback of original media resources.

Since SoundPool's load method does not support rawfile directory resources, we focus on AVPlayer for audio playback.

AVPlayer Playback Workflow

The full process includes: creating an AVPlayer, setting playback resources, configuring parameters (volume/speed/focus mode), playback control (play/pause/seek/stop), resetting, and destroying resources.

AVPlayer is similar to Android's MediaPlayer, maintaining a state machine. You can actively get the current state via the state property or listen for changes using on('stateChange'). Operating on an AVPlayer in an error state may throw exceptions or cause undefined behavior.

Ringing Tool: RingAVPlayer

  1. Create the player: let avPlayer: media.AVPlayer = await media.createAVPlayer();
  2. Set state change callbacks: avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {});
  3. Get the local audio resource FD: let fileDescriptor = await context.resourceManager.getRawFd('ring.mp3');
  4. Assign fdSrc to trigger the initialized state: avPlayer.fdSrc = fileDescriptor;
  5. In the state callback, call avPlayer.prepare() after receiving initialized to trigger resource loading.
  6. In the state callback, call avPlayer.play() after receiving prepared; set avPlayer.loop = true before playback for looping.
  7. Call stop to end playback.
import { media } from '@kit.MediaKit';  
import { common } from '@kit.AbilityKit';  
import { BusinessError } from '@kit.BasicServicesKit';  
import { audio } from '@kit.AudioKit';  
import { Logg } from '../../../../../../../Index';  

const TAG = 'RingAVPlayer';  
export class RingAVPlayer {  
  private mAVPlayer: media.AVPlayer|undefined = undefined;  
  private count: number = 0;  

  // Register AVPlayer callback functions  
  setAVPlayerCallback(avPlayer: media.AVPlayer) {  
    // Seek operation result callback  
    avPlayer.on('seekDone', (seekDoneTime: number) => {  
      Logg.i(TAG, `AVPlayer seek succeeded, seek time is ${seekDoneTime}`);  
    })  
    // Error callback: call reset when an error occurs  
    avPlayer.on('error', (err: BusinessError) => {  
      Logg.e(TAG, `Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);  
      avPlayer.reset(); // Reset resources, trigger idle state  
    })  
    // State machine change callback  
    avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => {  
      switch (state) {  
        case 'idle':  
          console.info('AVPlayer state idle called.');  
          avPlayer.release();  
          break;  
        case 'initialized':  
          Logg.i(TAG, 'AVPlayer state initialized called.');  
          avPlayer.audioRendererInfo = {  
            usage: audio.StreamUsage.STREAM_USAGE_MUSIC,  
            rendererFlags: 0  
          }  
          avPlayer.prepare();  
          break;  
        case 'prepared':  
          Logg.i(TAG, 'AVPlayer state prepared called.');  
          avPlayer.loop = true;  
          avPlayer.play();  
          break;  
        case 'playing':  
          Logg.i(TAG, 'AVPlayer state playing called.');  
          break;  
        case 'paused':  
          Logg.i(TAG, 'AVPlayer state paused called.');  
          break;  
        case 'completed':  
          Logg.i(TAG, 'AVPlayer state completed called.');  
          break;  
        case 'stopped':  
          Logg.i(TAG, 'AVPlayer state stopped called.');  
          break;  
        case 'released':  
          Logg.i(TAG, 'AVPlayer state released called.');  
          break;  
        default:  
          Logg.i(TAG, 'AVPlayer state unknown called.');  
          break;  
      }  
    })  
  }  

  // Demo: Use resource manager to get media files in HAP and play via fdSrc  
  async startAVPlayer() {  
    this.mAVPlayer = await media.createAVPlayer();  
    this.setAVPlayerCallback(this.mAVPlayer);  
    let context = getContext(this) as common.UIAbilityContext;  
    let fileDescriptor = await context.resourceManager.getRawFd('call_music.mp3');  
    let avFileDescriptor: media.AVFileDescriptor =  
      { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };  
    this.mAVPlayer.fdSrc = avFileDescriptor;  
  }  

  stopAVPlayer() {  
    if (this.mAVPlayer) {  
      this.mAVPlayer.stop();  
      this.mAVPlayer.release();  
    }  
  }  
}
Enter fullscreen mode Exit fullscreen mode

3. Vibration Implementation

On Android, Vibrator is used as follows:

protected long[] pattern = { 1000, 1000, 1000, 1000 };
mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);  
mVibrator.vibrate(pattern, 0);
Enter fullscreen mode Exit fullscreen mode

The array elements represent vibration durations (ms). The first element is the wait time before vibration, and subsequent elements alternate between vibration and pause. repeat determines repetition: -1 for no repeat, 0 for continuous vibration. { 1000, 1000, 1000, 1000 } means wait 1s, vibrate 1s, wait 1s, vibrate 1s, repeating. Call cancel to stop.

HarmonyOS Next provides corresponding vibration capabilities via the Vibrator module, supporting:

  • Key presses with different intensities and durations
  • Alarms and calls with single or periodic vibrations of varying intensities and durations

Three Vibration Types

Name Description
Fixed-duration vibration Specify a duration; the motor vibrates at default intensity and frequency. See VibrateTime.
Preset vibration System-defined vibration effects for fixed scenarios (e.g., haptic.clock.timer for timer adjustments). See VibratePreset.
Custom vibration Allows users to design vibration effects via configuration files. See VibrateFromFile.

Example Code

Fixed-Duration Vibration
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

try {
  vibrator.startVibration({
    type: 'time',
    duration: 1000,
  }, {
    id: 0,
    usage: 'alarm'
  }, (error: BusinessError) => {
    if (error) {
      console.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`);
      return;
    }
    console.info('Succeed in starting vibration');
  });
} catch (err) {
  let e: BusinessError = err as BusinessError;
  console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}

// Stop fixed-duration vibration
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

try {
  vibrator.stopVibration(vibrator.VibratorStopMode.VIBRATOR_STOP_MODE_TIME, (error: BusinessError) => {
    if (error) {
      console.error(`Failed to stop vibration. Code: ${error.code}, message: ${error.message}`);
      return;
    }
    console.info('Succeed in stopping vibration');
  })
} catch (err) {
  let e: BusinessError = err as BusinessError;
  console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
Enter fullscreen mode Exit fullscreen mode
Preset Vibration
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

try {
  vibrator.isSupportEffect('haptic.effect.soft', (err: BusinessError, state: boolean) => {
    if (err) {
      console.error(`Failed to query effect. Code: ${err.code}, message: ${err.message}`);
      return;
    }
    console.info('Succeed in querying effect');
    if (state) {
      vibrator.startVibration({
        type: 'preset',
        effectId: 'haptic.effect.soft',
        count: 1,
        intensity: 50,
      }, {
        usage: 'unknown'
      }, (error: BusinessError) => {
        if (error) {
          console.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`);
        } else {
          console.info('Succeed in starting vibration');
        }
      });
    }
  })
} catch (error) {
  let e: BusinessError = error as BusinessError;
  console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}

// Stop preset vibration
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

try {
  vibrator.stopVibration(vibrator.VibratorStopMode.VIBRATOR_STOP_MODE_PRESET, (error: BusinessError) => {
    if (error) {
      console.error(`Failed to stop vibration. Code: ${error.code}, message: ${error.message}`);
      return;
    }
    console.info('Succeed in stopping vibration');
  })
} catch (err) {
  let e: BusinessError = err as BusinessError;
  console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
Enter fullscreen mode Exit fullscreen mode
Custom Vibration from File
import { vibrator } from '@kit.SensorServiceKit';
import { resourceManager } from '@kit.LocalizationKit';
import { BusinessError } from '@kit.BasicServicesKit';

const fileName: string = 'xxx.json';
let rawFd: resourceManager.RawFileDescriptor = getContext().resourceManager.getRawFdSync(fileName);

try {
  vibrator.startVibration({
    type: "file",
    hapticFd: { fd: rawFd.fd, offset: rawFd.offset, length: rawFd.length }
  }, {
    id: 0,
    usage: 'alarm'
  }, (error: BusinessError) => {
    if (error) {
      console.error(`Failed to start vibration. Code: ${error.code}, message: ${error.message}`);
      return;
    }
    console.info('Succeed in starting vibration');
  });
} catch (err) {
  let e: BusinessError = err as BusinessError;
  console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}

getContext().resourceManager.closeRawFdSync(fileName);

// Stop all vibrations
import { vibrator } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

try {
  vibrator.stopVibration((error: BusinessError) => {
    if (error) {
      console.error(`Failed to stop vibration. Code: ${error.code}, message: ${error.message}`);
      return;
    }
    console.info('Succeed in stopping vibration');
  })
} catch (error) {
  let e: BusinessError = error as BusinessError;
  console.error(`An unexpected error occurred. Code: ${e.code}, message: ${e.message}`);
}
Enter fullscreen mode Exit fullscreen mode

4. Reference Documents

5. Summary

This article introduces implementing custom ringing and vibration effects on call pages in HarmonyOS Next audio-video calling scenarios, focusing on AVPlayer and vibrator APIs.

Top comments (0)