Compare commits

...

7 Commits
pr3979 ... mic

Author SHA1 Message Date
Romain Vimont
ff5ffc892f Add option to select audio source
Pass --audio-source=mic to capture the microphone instead of the device
audio output.
2023-06-01 09:21:09 +02:00
Romain Vimont
360f2fea1e Extract AudioCapture creation
This will allow to pass capture options without code duplication.
2023-06-01 09:21:09 +02:00
Adonis Najimi
24999d0d32 Reset video capture on folding event
Handle folding event the same way as rotation events.

Fixes #3960 <https://github.com/Genymobile/scrcpy/issues/3960>
PR #3979 <https://github.com/Genymobile/scrcpy/pull/3979>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-06-01 09:20:00 +02:00
Romain Vimont
8e2c0d6407 Rename rotationChanged to resetCapture
The flag is used to reset the capture (restart the encoding) on rotation
change. It will also be used for other events (on folding change), so
rename it.

PR #3979 <https://github.com/Genymobile/scrcpy/pull/3979>
2023-06-01 09:20:00 +02:00
Romain Vimont
9a2abba098 Update demuxer comment
The comment was outdated:
 - the "meta" header is now always present (not only when recording is
   enabled);
 - it is not only used for the video stream, but also for the audio
   stream.
2023-06-01 09:04:00 +02:00
shuax
b2d860382f Fix stream offset on audio buffer underflow
The `read` variable is in number of samples, while the offset must be in
bytes.

PR #4045 <https://github.com/Genymobile/scrcpy/pull/4045>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-31 10:09:56 +02:00
Romain Vimont
4c4a03ebe1 Reorder options to maintain alphabetical order 2023-05-30 21:36:48 +02:00
22 changed files with 236 additions and 44 deletions

View File

@@ -7,6 +7,7 @@ _scrcpy() {
--audio-codec=
--audio-codec-options=
--audio-encoder=
--audio-source=
--audio-output-buffer=
-b --video-bit-rate=
--crop=
@@ -86,6 +87,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
return
;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
return

View File

@@ -14,6 +14,7 @@ arguments=(
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic)'
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'

View File

@@ -33,14 +33,6 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru
Default is 50.
.TP
.BI "\-\-audio\-output\-buffer ms
Configure the size of the SDL audio output buffer (in milliseconds).
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
Default is 5.
.TP
.BI "\-\-audio\-codec " name
Select an audio codec (opus, aac or raw).
@@ -63,6 +55,20 @@ Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\
The available encoders can be listed by \-\-list\-encoders.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output or mic).
Default is output.
.TP
.BI "\-\-audio\-output\-buffer ms
Configure the size of the SDL audio output buffer (in milliseconds).
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
Default is 5.
.TP
.BI "\-b, \-\-video\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).

View File

@@ -107,7 +107,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
// latency.
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(stream + read, 0, TO_BYTES(silence));
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
if (ap->received) {
// Inserting additional samples immediately increases buffering

View File

@@ -76,6 +76,7 @@ enum {
OPT_NO_VIDEO,
OPT_NO_AUDIO_PLAYBACK,
OPT_NO_VIDEO_PLAYBACK,
OPT_AUDIO_SOURCE,
};
struct sc_option {
@@ -134,16 +135,6 @@ static const struct sc_option options[] = {
"likelyhood of buffer underrun (causing audio glitches).\n"
"Default is 50.",
},
{
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
.longopt = "audio-output-buffer",
.argdesc = "ms",
.text = "Configure the size of the SDL audio output buffer (in "
"milliseconds).\n"
"If you get \"robotic\" audio playback, you should test with "
"a higher value (10). Do not change this setting otherwise.\n"
"Default is 5.",
},
{
.longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec",
@@ -171,6 +162,23 @@ static const struct sc_option options[] = {
"codec provided by --audio-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
"Default is output.",
},
{
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
.longopt = "audio-output-buffer",
.argdesc = "ms",
.text = "Configure the size of the SDL audio output buffer (in "
"milliseconds).\n"
"If you get \"robotic\" audio playback, you should test with "
"a higher value (10). Do not change this setting otherwise.\n"
"Default is 5.",
},
{
.shortopt = 'b',
.longopt = "video-bit-rate",
@@ -1588,6 +1596,22 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
return false;
}
static bool
parse_audio_source(const char *optarg, enum sc_audio_source *source) {
if (!strcmp(optarg, "mic")) {
*source = SC_AUDIO_SOURCE_MIC;
return true;
}
if (!strcmp(optarg, "output")) {
*source = SC_AUDIO_SOURCE_OUTPUT;
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
return false;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@@ -1915,6 +1939,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_AUDIO_SOURCE:
if (!parse_audio_source(optarg, &opts->audio_source)) {
return false;
}
break;
default:
// getopt prints the error message on stderr
return false;

View File

@@ -79,9 +79,8 @@ sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
// The video and audio streams contain a sequence of raw packets (as
// provided by MediaCodec), each prefixed with a "meta" header.
//
// The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...

View File

@@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = {
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
.audio_source = SC_AUDIO_SOURCE_OUTPUT,
.record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,

View File

@@ -44,6 +44,11 @@ enum sc_codec {
SC_CODEC_RAW,
};
enum sc_audio_source {
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
};
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
@@ -115,6 +120,7 @@ struct scrcpy_options {
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
enum sc_audio_source audio_source;
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode;

View File

@@ -334,6 +334,7 @@ scrcpy(struct scrcpy_options *options) {
.log_level = options->log_level,
.video_codec = options->video_codec,
.audio_codec = options->audio_codec,
.audio_source = options->audio_source,
.crop = options->crop,
.port_range = options->port_range,
.tunnel_host = options->tunnel_host,

View File

@@ -246,6 +246,10 @@ execute_server(struct sc_server *server,
ADD_PARAM("audio_codec=%s",
sc_server_get_codec_name(params->audio_codec));
}
if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) {
assert(params->audio_source == SC_AUDIO_SOURCE_MIC);
ADD_PARAM("audio_source=mic");
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);
}

View File

@@ -26,6 +26,7 @@ struct sc_server_params {
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
enum sc_audio_source audio_source;
const char *crop;
const char *video_codec_options;
const char *audio_codec_options;

View File

@@ -41,6 +41,24 @@ interesting to add [buffering](#buffering) to minimize glitches:
scrcpy --no-video --audio-buffer=200
```
## Source
By default, the device audio output is forwarded.
It is possible to capture the device microphone instead:
```
scrcpy --audio-source=mic
```
For example, to use the device as a dictaphone and record a capture directly on
the computer:
```
scrcpy --audio-source=mic --no-video --no-audio-playback --record=file.opus
```
## Codec
The audio codec can be selected. The possible values are `opus` (default), `aac`

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
oneway interface IDisplayFoldListener
{
/** Called when the foldedness of a display changes */
void onDisplayFoldChanged(int displayId, boolean folded);
}

View File

@@ -10,7 +10,6 @@ import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
@@ -18,7 +17,6 @@ import java.nio.ByteBuffer;
public final class AudioCapture {
public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX;
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
@@ -26,12 +24,18 @@ public final class AudioCapture {
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2;
private final int audioSource;
private AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousPts = 0;
private long nextPts = 0;
public AudioCapture(AudioSource audioSource) {
this.audioSource = audioSource.value();
}
public static int millisToBytes(int millis) {
return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000;
}
@@ -46,13 +50,13 @@ public final class AudioCapture {
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
private static AudioRecord createAudioRecord(int audioSource) {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(SOURCE);
builder.setAudioSource(audioSource);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency
@@ -100,12 +104,12 @@ public final class AudioCapture {
private void startRecording() {
try {
recorder = createAudioRecord();
recorder = createAudioRecord(audioSource);
} catch (NullPointerException e) {
// Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
// - <https://github.com/Genymobile/scrcpy/issues/3805>
// - <https://github.com/Genymobile/scrcpy/pull/3862>
recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording();
}

View File

@@ -40,6 +40,7 @@ public final class AudioEncoder implements AsyncProcessor {
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
private final AudioCapture capture;
private final Streamer streamer;
private final int bitRate;
private final List<CodecOption> codecOptions;
@@ -58,7 +59,8 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended;
public AudioEncoder(Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
this.capture = capture;
this.streamer = streamer;
this.bitRate = bitRate;
this.codecOptions = codecOptions;
@@ -175,7 +177,6 @@ public final class AudioEncoder implements AsyncProcessor {
}
MediaCodec mediaCodec = null;
AudioCapture capture = new AudioCapture();
boolean mediaCodecStarted = false;
try {
@@ -192,10 +193,9 @@ public final class AudioEncoder implements AsyncProcessor {
capture.start();
final MediaCodec mediaCodecRef = mediaCodec;
final AudioCapture captureRef = capture;
inputThread = new Thread(() -> {
try {
inputThread(mediaCodecRef, captureRef);
inputThread(mediaCodecRef, capture);
} catch (IOException | InterruptedException e) {
Ln.e("Audio capture error", e);
} finally {

View File

@@ -8,6 +8,7 @@ import java.nio.ByteBuffer;
public final class AudioRawRecorder implements AsyncProcessor {
private final AudioCapture capture;
private final Streamer streamer;
private Thread thread;
@@ -15,7 +16,8 @@ public final class AudioRawRecorder implements AsyncProcessor {
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
public AudioRawRecorder(Streamer streamer) {
public AudioRawRecorder(AudioCapture capture, Streamer streamer) {
this.capture = capture;
this.streamer = streamer;
}
@@ -29,7 +31,6 @@ public final class AudioRawRecorder implements AsyncProcessor {
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
AudioCapture capture = new AudioCapture();
try {
capture.start();

View File

@@ -0,0 +1,30 @@
package com.genymobile.scrcpy;
import android.media.MediaRecorder;
public enum AudioSource {
OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX),
MIC("mic", MediaRecorder.AudioSource.MIC);
private final String name;
private final int value;
AudioSource(String name, int value) {
this.name = name;
this.value = value;
}
int value() {
return value;
}
static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View File

@@ -12,6 +12,7 @@ import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
@@ -35,6 +36,10 @@ public final class Device {
void onRotationChanged(int rotation);
}
public interface FoldListener {
void onFoldChanged(int displayId, boolean folded);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
@@ -46,6 +51,7 @@ public final class Device {
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private FoldListener foldListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
@@ -93,6 +99,26 @@ public final class Device {
}
}, displayId);
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
synchronized (Device.this) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
return;
}
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
options.getMaxSize(), options.getLockVideoOrientation());
// notify
if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded);
}
}
}
});
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
@@ -224,6 +250,10 @@ public final class Device {
this.rotationListener = rotationListener;
}
public synchronized void setFoldListener(FoldListener foldlistener) {
this.foldListener = foldlistener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}

View File

@@ -14,6 +14,7 @@ public class Options {
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS;
private AudioSource audioSource = AudioSource.OUTPUT;
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private int maxFps;
@@ -72,6 +73,10 @@ public class Options {
return audioCodec;
}
public AudioSource getAudioSource() {
return audioSource;
}
public int getVideoBitRate() {
return videoBitRate;
}
@@ -225,6 +230,13 @@ public class Options {
}
options.audioCodec = audioCodec;
break;
case "audio_source":
AudioSource audioSource = AudioSource.findByName(value);
if (audioSource == null) {
throw new IllegalArgumentException("Audio source " + value + " not supported");
}
options.audioSource = audioSource;
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;

View File

@@ -16,7 +16,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@@ -26,7 +26,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final AtomicBoolean resetCapture = new AtomicBoolean();
private final Device device;
private final Streamer streamer;
@@ -54,12 +54,17 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
}
@Override
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
public void onFoldChanged(int displayId, boolean folded) {
resetCapture.set(true);
}
private boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
@Override
public void onRotationChanged(int rotation) {
resetCapture.set(true);
}
private boolean consumeResetCapture() {
return resetCapture.getAndSet(false);
}
private void streamScreen() throws IOException, ConfigurationException {
@@ -68,6 +73,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
device.setFoldListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
@@ -115,6 +121,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
} finally {
mediaCodec.release();
device.setRotationListener(null);
device.setFoldListener(null);
SurfaceControl.destroyDisplay(display);
}
}
@@ -169,14 +176,14 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
while (!consumeResetCapture() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (consumeRotationChange()) {
if (consumeResetCapture()) {
// must restart encoding with new size
break;
}

View File

@@ -136,13 +136,14 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioStreamer);
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else {
audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
}
asyncProcessors.add(audioRecorder);

View File

@@ -4,6 +4,7 @@ import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -108,4 +109,13 @@ public final class WindowManager {
throw new AssertionError(e);
}
}
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
Class<?> cls = manager.getClass();
cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}