(Android-RTC-4)分析createPeerConnectionFactory之AudioDeviceModule

上图是之前 Android-RTC-1 简单分析得出来的,一张关于PeerConnectionFactory的至下而上结构图。今天继续从PeerConnectionFactory入手,承接 Android-RTC-3 的内容,分析createPeerConnectionFactory。

PeerConnectionFactory之PeerConnectionFactory.createPeerConnectionFactory。

从上图可以粗略的知道PeerConnectionFactory有三个主要模块,AudioDeviceModule / VideoEncoder/DecoderFactory / NetworkOptions,我们先来聚焦一下demo的入参情况。

// AppRTCDemo.PeerConnectionClient.java
private void createPeerConnectionFactoryInternal(PeerConnectionFactory.Options options) {
    // ... ...
    final AudioDeviceModule adm = createJavaAudioDevice();
    final boolean enableH264HighProfile =
            VIDEO_CODEC_H264_HIGH.equals(peerConnectionParameters.videoCodec);
    final VideoEncoderFactory encoderFactory;
    final VideoDecoderFactory decoderFactory;
    if (peerConnectionParameters.videoCodecHwAcceleration) {
        encoderFactory = new DefaultVideoEncoderFactory(
                rootEglBase.getEglBaseContext(), true, enableH264HighProfile);
        decoderFactory = new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext());
    } else {
        encoderFactory = new SoftwareVideoEncoderFactory();
        decoderFactory = new SoftwareVideoDecoderFactory();
    }
    factory = PeerConnectionFactory.builder()
            .setOptions(options)
            .setAudioDeviceModule(adm)
            .setVideoEncoderFactory(encoderFactory)
            .setVideoDecoderFactory(decoderFactory)
            .createPeerConnectionFactory();
    Log.d(TAG, "Peer connection factory created.");
    adm.release();
}

public static class Options {
    static final int ADAPTER_TYPE_UNKNOWN = 0;
    static final int ADAPTER_TYPE_ETHERNET = 1;
    static final int ADAPTER_TYPE_WIFI = 2;
    static final int ADAPTER_TYPE_CELLULAR = 4;
    static final int ADAPTER_TYPE_VPN = 8;
    static final int ADAPTER_TYPE_LOOPBACK = 16;
    static final int ADAPTER_TYPE_ANY = 32;
    public int networkIgnoreMask;
    public boolean disableEncryption;
    public boolean disableNetworkMonitor;

    public Options() { }
    @CalledByNative("Options")
    int getNetworkIgnoreMask() {
        return this.networkIgnoreMask;
    }
    @CalledByNative("Options")
    boolean getDisableEncryption() {
        return this.disableEncryption;
    }
    @CalledByNative("Options")
    boolean getDisableNetworkMonitor() {
        return this.disableNetworkMonitor;
    }
}

PeerConnectionFactory.Options只是简单的new了一个默认构造,重点还是要放在 AudioDeviceModule / VideoEncoderFactory / VideoDecoderFactory。

一、JavaAudioDeviceModule

// AppWebRTC-demo中的PeerConnectionClient.java
AudioDeviceModule createJavaAudioDevice() {
    if (!peerConnectionParameters.useOpenSLES) {
        Log.w(TAG, "External OpenSLES ADM not implemented yet.");
        // TODO: Add support for external OpenSLES ADM.
    }
    // Set audio record error callbacks.
    JavaAudioDeviceModule.AudioRecordErrorCallback audioRecordErrorCallback;
	// Set audio track error callbacks.
    JavaAudioDeviceModule.AudioTrackErrorCallback audioTrackErrorCallback;
    // Set audio record state callbacks.
    JavaAudioDeviceModule.AudioRecordStateCallback audioRecordStateCallback;
    // Set audio track state callbacks.
    JavaAudioDeviceModule.AudioTrackStateCallback audioTrackStateCallback;
    // 篇幅关系就不把代码贴全了。
    return JavaAudioDeviceModule.builder(appContext)
            .setSamplesReadyCallback(saveRecordedAudioToFile)
            .setUseHardwareAcousticEchoCanceler(!peerConnectionParameters.disableBuiltInAEC)
            .setUseHardwareNoiseSuppressor(!peerConnectionParameters.disableBuiltInNS)
            .setAudioRecordErrorCallback(audioRecordErrorCallback)
            .setAudioTrackErrorCallback(audioTrackErrorCallback)
            .setAudioRecordStateCallback(audioRecordStateCallback)
            .setAudioTrackStateCallback(audioTrackStateCallback)
            .createAudioDeviceModule();
}
// org.webrtc.audio.JavaAudioDeviceModule.java
public static class Builder {
    private final Context context;
    private final AudioManager audioManager;
    private int inputSampleRate = WebRtcAudioManager.getSampleRate(audioManager);
    private int outputSampleRate = WebRtcAudioManager.getSampleRate(audioManager);
    private int audioSource = WebRtcAudioRecord.DEFAULT_AUDIO_SOURCE;
    private int audioFormat = WebRtcAudioRecord.DEFAULT_AUDIO_FORMAT;
    private AudioTrackErrorCallback audioTrackErrorCallback;
    private AudioRecordErrorCallback audioRecordErrorCallback;
    private SamplesReadyCallback samplesReadyCallback;
    private AudioTrackStateCallback audioTrackStateCallback;
    private AudioRecordStateCallback audioRecordStateCallback;
    private boolean useHardwareAcousticEchoCanceler = isBuiltInAcousticEchoCancelerSupported();
    private boolean useHardwareNoiseSuppressor = isBuiltInNoiseSuppressorSupported();
    private boolean useStereoInput;
    private boolean useStereoOutput;

    public AudioDeviceModule createAudioDeviceModule() {
        if (useHardwareNoiseSuppressor) {
            Logging.d(TAG, "HW NS will be used.");
        } else {
            if (isBuiltInNoiseSuppressorSupported()) {
                Logging.d(TAG, "Overriding default behavior; now using WebRTC NS!");
            }
            Logging.d(TAG, "HW NS will not be used.");
        }
        if (useHardwareAcousticEchoCanceler) {
            Logging.d(TAG, "HW AEC will be used.");
        } else {
            if (isBuiltInAcousticEchoCancelerSupported()) {
                Logging.d(TAG, "Overriding default behavior; now using WebRTC AEC!");
            }
            Logging.d(TAG, "HW AEC will not be used.");
        }
        final WebRtcAudioRecord audioInput = new WebRtcAudioRecord(context, audioManager, audioSource, audioFormat, audioRecordErrorCallback, audioRecordStateCallback, samplesReadyCallback, useHardwareAcousticEchoCanceler, useHardwareNoiseSuppressor);
        final WebRtcAudioTrack audioOutput = new WebRtcAudioTrack(context, audioManager, audioTrackErrorCallback, audioTrackStateCallback);
        return new JavaAudioDeviceModule(context, audioManager, audioInput, audioOutput, inputSampleRate, outputSampleRate, useStereoInput, useStereoOutput);
    }
}

1、首先从AudioDeviceModule入手,分析java层代码。音频模块涉及回音消除和降噪,android原生系统已经抽象实现了相关功能模块,但是质量效果的优劣取决于硬件设备。可以往下跟踪 isBuiltInAcousticEchoCancelerSupported / isBuiltInNoiseSuppressorSupported两个静态方法,专门规避了AOSP中的软实现回音消除/降噪功能。尽可能的使用硬件的回音消除和降噪的功能。

// 专们特指定 AEC特性 的UUID,相当于识别码
public static final UUID EFFECT_TYPE_AEC = UUID   
            .fromString("7b491460-8d4d-11e0-bd61-0002a5d5c51b");
// UUIDs for Software Audio Effects that we want to avoid using.
// AOSP中软实现的回音消除模块的UUID,避免使用软实现。
private static final UUID AOSP_ACOUSTIC_ECHO_CANCELER =
            UUID.fromString("bb392ec0-8d4d-11e0-a896-0002a5d5c51b");
public static boolean isAcousticEchoCancelerSupported() {
    if (Build.VERSION.SDK_INT < 18)
            return false;
    return isEffectTypeAvailable(AudioEffect.EFFECT_TYPE_AEC, AOSP_ACOUSTIC_ECHO_CANCELER);
}
// Returns true if an effect of the specified type is available. Functionally
// equivalent to (NoiseSuppressor|AutomaticGainControl|...).isAvailable(), but
// faster as it avoids the expensive OS call to enumerate effects.
private static boolean isEffectTypeAvailable(UUID effectType, UUID blackListedUuid) {
    Descriptor[] effects = AudioEffect.queryEffects();
    if (effects == null) {
        return false;
    }
    for (Descriptor d : effects) {
        if (d.type.equals(effectType)) {
            return !d.uuid.equals(blackListedUuid);
        }
    }
    return false;
}

2、回头继续看createAudioDeviceModule当中的audioInput(WebRtcAudioRecord)和 audioOutput(WebRtcAudioTrack) 

看看WebRtcAudioRecord模块,结构图如上所示,大体上也是Android原生API——AudioRecord的封装。但是此模块的接口很多都打了自定义标签@CalledByNative,都是从native层调用的。具体逻辑还是要往下深入AudioDeviceModle才能一并展开。

同理,WebRtcAudioTrack也是对Android原生API——AudioTrack的封装。关键函数已经标记。

    private final Context context;
    private final AudioManager audioManager;
    private final WebRtcAudioRecord audioInput;
    private final WebRtcAudioTrack audioOutput;
    private final int inputSampleRate;
    private final int outputSampleRate;
    private final boolean useStereoInput;
    private final boolean useStereoOutput;

    private final Object nativeLock = new Object();
    private long nativeAudioDeviceModule;

    private JavaAudioDeviceModule(Context context, AudioManager audioManager,
                                  WebRtcAudioRecord audioInput, WebRtcAudioTrack audioOutput, int inputSampleRate,
                                  int outputSampleRate, boolean useStereoInput, boolean useStereoOutput) {
        this.context = context;
        this.audioManager = audioManager;
        this.audioInput = audioInput;
        this.audioOutput = audioOutput;
        this.inputSampleRate = inputSampleRate;
        this.outputSampleRate = outputSampleRate;
        this.useStereoInput = useStereoInput;
        this.useStereoOutput = useStereoOutput;
    }

最后我们回到JavaAudioDeviceModule.builder.createAudioDeviceModule,可以清晰明了的看到JavaAudioDeviceModule的成员变量,基本上都是Android系统关于音频相关的原生API,nativeAudioDeviceModule用于保存native层的对象地址值,相当于c++的对象指针。这种写法间接的把java对象和cpp对象建立起关联,在很多Android native开源项目是非常常见的。

二、jni层的AudioDeviceModule

分析完Java业务层的代码,顺着脉络深入native层分析。先回滚至Java层入口函数

//org.webrtc.PeerConnectionFactory.Builder
public PeerConnectionFactory createPeerConnectionFactory() {
    PeerConnectionFactory.checkInitializeHasBeenCalled();
    if (this.audioDeviceModule == null) {
        this.audioDeviceModule = JavaAudioDeviceModule.builder(ContextUtils.getApplicationContext()).createAudioDeviceModule();
    }
    return PeerConnectionFactory.nativeCreatePeerConnectionFactory(
    ContextUtils.getApplicationContext(), 
    this.options, 
    this.audioDeviceModule.getNativeAudioDeviceModulePointer(), 
    this.audioEncoderFactoryFactory.createNativeAudioEncoderFactory(), 
    this.audioDecoderFactoryFactory.createNativeAudioDecoderFactory(), 
    this.videoEncoderFactory,
    this.videoDecoderFactory,
    this.audioProcessingFactory == null ? 0L : this.audioProcessingFactory.createNative(),
    this.fecControllerFactoryFactory == null ? 0L : this.fecControllerFactoryFactory.createNative(),
    this.networkControllerFactoryFactory == null ? 0L : this.networkControllerFactoryFactory.createNativeNetworkControllerFactory(),
    this.networkStatePredictorFactoryFactory == null ? 0L : this.networkStatePredictorFactoryFactory.createNativeNetworkStatePredictorFactory(),
    this.neteqFactoryFactory == null ? 0L : this.neteqFactoryFactory.createNativeNetEqFactory()
);
}

以上是PeerConnectionFactory的createPeerConnectionFactory 创建工厂实体类的函数,这个入口函数以后会多次碰见,毕竟它承载了太多东西了;这里先着重分析AudioDeviceModule,即分析JavaAudioDeviceModule.getNativeAudioDeviceModulePointer。

//org.webrtc.audio.JavaAudioDeviceModule
public long getNativeAudioDeviceModulePointer() {
    synchronized(this.nativeLock) {
        if (this.nativeAudioDeviceModule == 0L) {
            this.nativeAudioDeviceModule = nativeCreateAudioDeviceModule(
                                            this.context, this.audioManager, 
                                            this.audioInput, this.audioOutput, 
                                            this.inputSampleRate, this.outputSampleRate, 
                                            this.useStereoInput, this.useStereoOutput);
        }
        return this.nativeAudioDeviceModule;
    }
}
// out\arm64-v8a\gen\sdk\android\generated_java_audio_jni\JavaAudioDeviceModule_jni.h
JNI_GENERATOR_EXPORT jlong
    Java_org_webrtc_audio_JavaAudioDeviceModule_nativeCreateAudioDeviceModule(
    JNIEnv* env,
    jclass jcaller,
    jobject context,
    jobject audioManager,
    jobject audioInput,
    jobject audioOutput,
    jint inputSampleRate,
    jint outputSampleRate,
    jboolean useStereoInput,
    jboolean useStereoOutput) {
  return JNI_JavaAudioDeviceModule_CreateAudioDeviceModule(env,
base::android::JavaParamRef<jobject>(env, context),
base::android::JavaParamRef<jobject>(env, audioManager),
base::android::JavaParamRef<jobject>(env, audioInput),
base::android::JavaParamRef<jobject>(env, audioOutput), 
inputSampleRate, outputSampleRate,
useStereoInput, useStereoOutput);
}
// sdk\android\src\jni\audio_device\java_audio_device_module.cc
static jlong JNI_JavaAudioDeviceModule_CreateAudioDeviceModule(
    JNIEnv* env,
    const JavaParamRef<jobject>& j_context,
    const JavaParamRef<jobject>& j_audio_manager,
    const JavaParamRef<jobject>& j_webrtc_audio_record,
    const JavaParamRef<jobject>& j_webrtc_audio_track,
    int input_sample_rate,
    int output_sample_rate,
    jboolean j_use_stereo_input,
    jboolean j_use_stereo_output) {
  AudioParameters input_parameters;
  AudioParameters output_parameters;
  GetAudioParameters(env, j_context, j_audio_manager, input_sample_rate,
                     output_sample_rate, j_use_stereo_input,
                     j_use_stereo_output, &input_parameters,
                     &output_parameters);
  auto audio_input = std::make_unique<AudioRecordJni>(
      env, input_parameters, kHighLatencyModeDelayEstimateInMilliseconds,
      j_webrtc_audio_record);
  auto audio_output = std::make_unique<AudioTrackJni>(env, output_parameters,
                                                      j_webrtc_audio_track);
  return jlongFromPointer(CreateAudioDeviceModuleFromInputAndOutput(
                              AudioDeviceModule::kAndroidJavaAudio,
                              j_use_stereo_input, j_use_stereo_output,
                              kHighLatencyModeDelayEstimateInMilliseconds,
                              std::move(audio_input), std::move(audio_output))
                              .release());
}

看着以上的调用链,层层封装,有点复杂。但不要方抓重点,两个AudioParameters对象input_parameters和output_parameters;两个智能指针对象AudioRecordJni(audio_input)和AudioTrackJni(audio_output),然后继续分析两个重点方法 GetAudioParameters / CreateAudioDeviceModuleFromInputAndOutput

// sdk\android\src\jni\audio_device\audio_device_module.cc
void GetAudioParameters(JNIEnv* env,
                        const JavaRef<jobject>& j_context,
                        const JavaRef<jobject>& j_audio_manager,
                        int input_sample_rate,
                        int output_sample_rate,
                        bool use_stereo_input,
                        bool use_stereo_output,
                        AudioParameters* input_parameters,
                        AudioParameters* output_parameters) {
  const int output_channels = use_stereo_output ? 2 : 1;
  const int input_channels = use_stereo_input ? 2 : 1;
  const size_t output_buffer_size = Java_WebRtcAudioManager_getOutputBufferSize(
      env, j_context, j_audio_manager, output_sample_rate, output_channels);
  const size_t input_buffer_size = Java_WebRtcAudioManager_getInputBufferSize(
      env, j_context, j_audio_manager, input_sample_rate, input_channels);
  output_parameters->reset(output_sample_rate,
                           static_cast<size_t>(output_channels),
                           static_cast<size_t>(output_buffer_size));
  input_parameters->reset(input_sample_rate,
                          static_cast<size_t>(input_channels),
                          static_cast<size_t>(input_buffer_size));
  RTC_CHECK(input_parameters->is_valid());
  RTC_CHECK(output_parameters->is_valid());
}
// org.webrtc.audio.WebRtcAudioManager.java
@CalledByNative
static int getOutputBufferSize(
        Context context, AudioManager audioManager, int sampleRate, int numberOfOutputChannels) {
    return isLowLatencyOutputSupported(context)
            ? getLowLatencyFramesPerBuffer(audioManager)
            : getMinOutputFrameSize(sampleRate, numberOfOutputChannels);
}
private static boolean isLowLatencyOutputSupported(Context context) {
    return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
}
private static int getLowLatencyFramesPerBuffer(AudioManager audioManager) {
    if (Build.VERSION.SDK_INT < 17) {
        return DEFAULT_FRAME_PER_BUFFER;
    }
    String framesPerBuffer =
            audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
    return framesPerBuffer == null ? DEFAULT_FRAME_PER_BUFFER : Integer.parseInt(framesPerBuffer);
}
private static int getMinOutputFrameSize(int sampleRateInHz, int numChannels) {
    final int bytesPerFrame = numChannels * (BITS_PER_SAMPLE / 8);
    final int channelConfig =
            (numChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO);
    return AudioTrack.getMinBufferSize(
            sampleRateInHz, channelConfig, AudioFormat.ENCODING_PCM_16BIT)
            / bytesPerFrame;
}

可以看到GetAudioParameters 也是反调用Java层的WebRtcAudioManager的两个系统方法。到这简单总结一下关于音频:采样率(sample rate)、采样位数(bit per sample)、通道数(channel number)、比特率or码率 的关系。

1、采样率是指每秒采样多少次。
2、采样位数是指每次数字采样的数据,用多少位数来保存,常见有8位或是16位。
3、通道常见单声道音频,立体声道,若是单声道,那么每次采样一个通道,若是立体声道,那么每次采样两个不同声道
4、比特率 = 采样率 * 采样位数 * 通道数(buffersize = 比特率 / 8)(纯理论)
5、实际情况系统都会进行一定的空间压缩优化。对于Android系统,可以使用系统API,AudioRecord.getMinBufferSize 和 AudioTrack.getMinBufferSize得出最小支持的size。

接下来分析CreateAudioDeviceModuleFromInputAndOutput。很简单,就是返回一个AndroidAudioDeviceModule对象的指针。

// sdk\android\src\jni\audio_device\audio_device_module.cc
rtc::scoped_refptr<AudioDeviceModule> CreateAudioDeviceModuleFromInputAndOutput(
    AudioDeviceModule::AudioLayer audio_layer,
    bool is_stereo_playout_supported,
    bool is_stereo_record_supported,
    uint16_t playout_delay_ms,
    std::unique_ptr<AudioInput> audio_input,
    std::unique_ptr<AudioOutput> audio_output) {
  RTC_DLOG(INFO) << __FUNCTION__;
  return rtc::make_ref_counted<AndroidAudioDeviceModule>(
      audio_layer, is_stereo_playout_supported, is_stereo_record_supported,
      playout_delay_ms, std::move(audio_input), std::move(audio_output));
}

到这里准备展开Native层的AudioDeviceModule——AndroidAudioDeviceModule,在此之前总结以上的分析内容。可以用以下这一张图简单概括。

AudioRecordJni对标WebRtcAudioRecord,AudioTrackJni对标WebRtcAudioTrack,nativeAudioDeviceModule保存着AndroidAudioDeviceModule对象指针的地址值。

三、AndroidAudioDeviceModule

犹豫篇幅和代码复杂度的关系,这一节不打算刨根问底的贴代码。先用一张图简单概述AndroidAudioDeviceModule的内部状态。

到这里能清晰的看清楚整个AudioDeviceModule的构成,接着再简单的分析AudioRecordJni——WebRtcAudioRecord的部分关键代码。AudioTrackJni——WebRtcAudioTrack暂时放一放,因为没有分析到传输模块,还没有外部音频数据的流入。

// sdk\android\src\jni\audio_device\audio_record_jni.cc
int32_t AudioRecordJni::InitRecording() {
  RTC_DCHECK(thread_checker_.IsCurrent());
  if (initialized_) {
    // Already initialized.
    return 0;
  }

  int frames_per_buffer = Java_WebRtcAudioRecord_initRecording(
      env_, j_audio_record_, audio_parameters_.sample_rate(),
      static_cast<int>(audio_parameters_.channels()));
  if (frames_per_buffer < 0) {
    direct_buffer_address_ = nullptr;
    return -1;
  }
  frames_per_buffer_ = static_cast<size_t>(frames_per_buffer);

  const size_t bytes_per_frame = audio_parameters_.channels() * sizeof(int16_t);

  initialized_ = true;
  return 0;
}
// org.webrtc.audio.WebRtcAudioRecord.java
@CalledByNative
private int initRecording(int sampleRate, int channels) {
    if (audioRecord != null) {
        return -1;
    }
    final int bytesPerFrame = channels * getBytesPerSample(audioFormat);
    final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
    byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);
    if (!(byteBuffer.hasArray())) {
        return -1;
    }
    emptyBytes = new byte[byteBuffer.capacity()];
	
    // 比起每次回调传递ByteBuffer(需要调用笨重的GetDirectBufferAddress方法)
	// 我们可以简单的传递一次缓存地址到内部native层。
    nativeCacheDirectBufferAddress(nativeAudioRecord, byteBuffer);
	
    final int channelConfig = channelCountToConfiguration(channels);
    // 请注意,此size大小不能保证在负载下顺利录制。
    int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
    if (minBufferSize == AudioRecord.ERROR || minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
        return -1;
    }
	// 使用 大于所需最小值的缓冲区空间,确保在负载下顺利录制。
	// 经验证,它不会增加实际记录延迟。
	// BUFFER_SIZE_FACTOR = 2
    int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
    try {
        if (Build.VERSION.SDK_INT >= 23) {
            this.audioRecord = createAudioRecordOnMOrHigher(this.audioSource, sampleRate, channelConfig, this.audioFormat, bufferSizeInBytes);
            if (this.preferredDevice != null) {
                this.setPreferredDevice(this.preferredDevice);
            }
        } else {
            this.audioRecord = createAudioRecordOnLowerThanM(this.audioSource, sampleRate, channelConfig, this.audioFormat, bufferSizeInBytes);
        }
    } catch (IllegalArgumentException e) {
        releaseAudioResources();
        return -1;
    }

    if (this.audioRecord != null && this.audioRecord.getState() == 1) {
        this.effects.enable(this.audioRecord.getAudioSessionId());
		// 启用硬件降噪,回音消除的特性。
        this.logMainParameters();
        this.logMainParametersExtended();
        return framesPerBuffer;
    } else {
        this.releaseAudioResources();
        return -1;
    }
}

以初始化 initRecording 为例。代码流程如上,其他方法也是这样一一对应。可能有同学会问,那什么才真正的调用init / start / stop之类的功能函数呢?不要急,因为现在还停留在初始化的构造阶段,往后分析功能的时候就能知道了。

本章总结:Android-RTC-AudioDeviceModule模块构造如下,希望下图对你有帮助。

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: List<Ticket> getAllTickets() { return ticketDAO.getAllTickets(); } @Override public void addTicket(Ticket ticket) { ticketDAO.addTicket(ticket); } } ``` 6. 创建View层。在View层中,你需要编Temper-RTC是一种集成了温度传感器和实时时钟(RTC)功能的电子元器写一些页面,用于显示用户信息和票务信息。这些页面需要调用Service层的方法,并将返回的件。它通常用于需要同时测量温度和计时的应用场景,例如温度控制系统、数据显示在页面上。代码如下: - user.jsp ```jsp <%@ page language="java" contentType="text/html;环境监测系统、电子钟表等。 Temper-RTC的温度传感器通常采用数字式温 charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/j度传感器,可以提供高精度、高稳定性的温度测量结果。其RTC功能则可以提sp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>供高精度的时间计数和日期记录,可以用于实现时间戳、定时器、日历等功能。 个人中心</title> </head> <body> <h1>个人中心</h1> <h2在使用Temper-RTC时,需要注意正确配置温度传感器和RTC参数,以确保其测量精度>用户信息</h2> <table> <thead> <tr> <th>ID</th> <th和计时精度符合要求。同时,还需要注意电源和引脚连接等方面的问题,以避>用户名</th> <th>邮箱</th> </tr> </thead> <tbody> <tr免电路干扰和信号损失等问题。 ### 回答2: temper-rtc是一种温度记录器和控制器。它主要用于监测和控制温度,广泛应用于实验室、医疗、食品加工和其他需要精确温度控制的领域。 temper-rtc具有高精度的温度测量能力,可以实时监测温度变化,并显示在屏幕上。同时,它还可以将温度数据存储在内部存储器中,以便之后进行数据分析和记录。此外,temper-rtc还可以通过无线或有线方式与计算机或其他外部设备连接,以便实现远程监控和控制。 temper-rtc还具有温度控制功能。用户可以设置温度上下限,并根据实际需求进行调整。一旦温度超过或低于设定的限制,temper-rtc将自动触发报警系统,以提醒用户温度异常。此外,temper-rtc还可以根据预设的控制参数自动调整温度控制设备的工作,以维持温度稳定。 temper-rtc具有小巧、便携和易于操作的特点。它通常由一个控制器和一个温度传感器组成,用户可以轻松携带和安装。控制器上配备了直观的操作界面和显示屏,用户可以方便地设置参数和查看温度信息。 总而言之,temper-rtc是一种功能强大的温度记录器和控制器,可以准确测量和监控温度,并实现远程控制和报警功能。它在各种需要精确温度控制的环境中广泛应用,为用户提供了方便、灵活和可靠的温度管理解决方案。 ### 回答3: temper-rtc是一款基于实时时钟(RTC)的温度传感器。RTC是一种能够实时测量和记录时间的电子设备,常用于计算机、电子设备和检测设备中。temper-rtc结合了RTC技术和温度传感器技术,提供了实时测量环境温度的功能。 temper-rtc的工作原理是通过温度传感器实时感知环境的温度,并将温度数据传输给RTCRTC通过内部的时钟来记录测量的时间,并保存温度数据。用户可以通过连接temper-rtc与电脑或其他设备,读取温度数据并进行分析和处理。 temper-rtc有多种用途。首先,它可以用于监控环境温度,例如在实验室、仓库或其他需要保持特定温度的场所。其次,temper-rtc可以用于数据记录和分析。用户可以使用temper-rtc来记录温度变化情况,比如在气象研究中分析气温的变化趋势。最后,temper-rtc也可以用于温度控制系统中。通过实时监测温度,并根据预设条件调整环境温度,实现温度的自动调控。 总之,temper-rtc是一款基于实时时钟的温度传感器,结合了RTC技术和温度传感器技术,能够实时感知环境温度并记录温度数据。它有广泛的应用领域,包括环境监测、数据记录和分析以及温度控制系统中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值