diff --git a/QtScrcpy/CMakeLists.txt b/QtScrcpy/CMakeLists.txt index 5ddf143..81b642d 100755 --- a/QtScrcpy/CMakeLists.txt +++ b/QtScrcpy/CMakeLists.txt @@ -79,8 +79,8 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network REQUIRED) +find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network Multimedia REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network Multimedia REQUIRED) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(QT NAMES Qt6 Qt5 COMPONENTS X11Extras REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED) @@ -108,6 +108,13 @@ set(QC_UIBASE_SOURCES ) source_group(uibase FILES ${QC_UIBASE_SOURCES}) +# audio +set(QC_AUDIO_SOURCES + audio/audiooutput.h + audio/audiooutput.cpp +) +source_group(audio FILES ${QC_AUDIO_SOURCES}) + # ui set(QC_UI_SOURCES ui/toolform.h @@ -199,6 +206,7 @@ set(QC_PROJECT_SOURCES ${QC_MAIN_SOURCES} ${QC_GROUP_CONTROLLER} ${QC_PLANTFORM_SOURCES} + ${QC_AUDIO_SOURCES} ) if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") @@ -242,6 +250,11 @@ set_target_properties(${PROJECT_NAME} PROPERTIES if(CMAKE_SYSTEM_NAME STREQUAL "Windows") get_target_property(QSC_BIN_OUTPUT_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY) set(QSC_DEPLOY_PATH ${QSC_BIN_OUTPUT_PATH}) + + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.bat" "${QSC_BIN_OUTPUT_PATH}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sndcpy/sndcpy.apk" "${QSC_BIN_OUTPUT_PATH}" + ) endif() # MacOS @@ -309,5 +322,6 @@ add_subdirectory(QtScrcpyCore) target_link_libraries(${PROJECT_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::Multimedia QtScrcpyCore ) diff --git a/QtScrcpy/audio/audiooutput.cpp b/QtScrcpy/audio/audiooutput.cpp new file mode 100644 index 0000000..db5c88e --- /dev/null +++ b/QtScrcpy/audio/audiooutput.cpp @@ -0,0 +1,178 @@ +#include +#include +#include +#include + +#include "audiooutput.h" + +AudioOutput::AudioOutput(QObject *parent) + : QObject(parent) +{ + connect(&m_sndcpy, &QProcess::readyReadStandardOutput, this, [this]() { + qInfo() << QString("AudioOutput::") << QString(m_sndcpy.readAllStandardOutput()); + }); + connect(&m_sndcpy, &QProcess::readyReadStandardError, this, [this]() { + qInfo() << QString("AudioOutput::") << QString(m_sndcpy.readAllStandardError()); + }); +} + +AudioOutput::~AudioOutput() +{ + if (QProcess::NotRunning != m_sndcpy.state()) { + m_sndcpy.kill(); + } + stop(); +} + +bool AudioOutput::start(const QString& serial, int port) +{ + if (m_running) { + stop(); + } + + QElapsedTimer timeConsumeCount; + timeConsumeCount.start(); + bool ret = runSndcpyProcess(serial, port); + qInfo() << "AudioOutput::run sndcpy cost:" << timeConsumeCount.elapsed() << "milliseconds"; + if (!ret) { + return ret; + } + + startAudioOutput(); + startRecvData(port); + + m_running = true; + return true; +} + +void AudioOutput::stop() +{ + if (!m_running) { + return; + } + m_running = false; + + stopRecvData(); + stopAudioOutput(); +} + +void AudioOutput::installonly(const QString &serial, int port) +{ + runSndcpyProcess(serial, port); +} + +bool AudioOutput::runSndcpyProcess(const QString &serial, int port) +{ + if (QProcess::NotRunning != m_sndcpy.state()) { + m_sndcpy.kill(); + } + + QStringList params; + params << serial; + params << QString("%1").arg(port); + m_sndcpy.start("sndcpy.bat", params); +/* + if (!m_sndcpy.waitForStarted()) { + qWarning() << "AudioOutput::start sndcpy.bat failed"; + return false; + } + if (!m_sndcpy.waitForFinished()) { + qWarning() << "AudioOutput::sndcpy.bat crashed"; + return false; + } +*/ + return true; +} + +void AudioOutput::startAudioOutput() +{ + if (m_audioOutput) { + return; + } + + QAudioFormat format; + format.setSampleRate(48000); + format.setChannelCount(2); + format.setSampleSize(16); + format.setCodec("audio/pcm"); + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::UnSignedInt); + + QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice()); + if (!info.isFormatSupported(format)) { + qWarning() << "AudioOutput::audio format not supported, cannot play audio."; + return; + } + + m_audioOutput = new QAudioOutput(format, this); + connect(m_audioOutput, &QAudioOutput::stateChanged, this, [](QAudio::State state) { + qInfo() << "AudioOutput::audio state changed:" << state; + }); + m_audioOutput->setBufferSize(48000*2*15/1000 * 20); + m_outputDevice = m_audioOutput->start(); +} + +void AudioOutput::stopAudioOutput() +{ + if (!m_audioOutput) { + return; + } + + m_audioOutput->stop(); + delete m_audioOutput; + m_audioOutput = nullptr; +} + +void AudioOutput::startRecvData(int port) +{ + if (m_workerThread.isRunning()) { + stopRecvData(); + } + + auto audioSocket = new QTcpSocket(); + audioSocket->moveToThread(&m_workerThread); + connect(&m_workerThread, &QThread::finished, audioSocket, &QObject::deleteLater); + + connect(this, &AudioOutput::connectTo, audioSocket, [audioSocket](int port) { + audioSocket->connectToHost(QHostAddress::LocalHost, port); + if (!audioSocket->waitForConnected(500)) { + qWarning("AudioOutput::audio socket connect failed"); + return; + } + qInfo("AudioOutput::audio socket connect success"); + }); + connect(audioSocket, &QIODevice::readyRead, audioSocket, [this, audioSocket]() { + qint64 recv = audioSocket->bytesAvailable(); + //qDebug() << "AudioOutput::recv data:" << recv; + + if (!m_outputDevice) { + return; + } + if (m_buffer.capacity() < recv) { + m_buffer.reserve(recv); + } + + qint64 count = audioSocket->read(m_buffer.data(), audioSocket->bytesAvailable()); + m_outputDevice->write(m_buffer.data(), count); + }); + connect(audioSocket, &QTcpSocket::stateChanged, audioSocket, [](QAbstractSocket::SocketState state) { + qInfo() << "AudioOutput::audio socket state changed:" << state; + + }); + connect(audioSocket, &QTcpSocket::errorOccurred, audioSocket, [](QAbstractSocket::SocketError error) { + qInfo() << "AudioOutput::audio socket error occurred:" << error; + }); + + m_workerThread.start(); + emit connectTo(port); +} + +void AudioOutput::stopRecvData() +{ + if (!m_workerThread.isRunning()) { + return; + } + + m_workerThread.quit(); + m_workerThread.wait(); +} diff --git a/QtScrcpy/audio/audiooutput.h b/QtScrcpy/audio/audiooutput.h new file mode 100644 index 0000000..702ea6d --- /dev/null +++ b/QtScrcpy/audio/audiooutput.h @@ -0,0 +1,41 @@ +#ifndef AUDIOOUTPUT_H +#define AUDIOOUTPUT_H + +#include +#include +#include +#include + +class QAudioOutput; +class QIODevice; +class AudioOutput : public QObject +{ + Q_OBJECT +public: + explicit AudioOutput(QObject *parent = nullptr); + ~AudioOutput(); + + bool start(const QString& serial, int port); + void stop(); + void installonly(const QString& serial, int port); + +private: + bool runSndcpyProcess(const QString& serial, int port); + void startAudioOutput(); + void stopAudioOutput(); + void startRecvData(int port); + void stopRecvData(); + +signals: + void connectTo(int port); + +private: + QAudioOutput* m_audioOutput = nullptr; + QPointer m_outputDevice; + QThread m_workerThread; + QProcess m_sndcpy; + QVector m_buffer; + bool m_running = false; +}; + +#endif // AUDIOOUTPUT_H diff --git a/QtScrcpy/res/i18n/en_US.qm b/QtScrcpy/res/i18n/en_US.qm index 9a830c4..f0d5294 100644 Binary files a/QtScrcpy/res/i18n/en_US.qm and b/QtScrcpy/res/i18n/en_US.qm differ diff --git a/QtScrcpy/res/i18n/en_US.ts b/QtScrcpy/res/i18n/en_US.ts index a596510..d51a414 100644 --- a/QtScrcpy/res/i18n/en_US.ts +++ b/QtScrcpy/res/i18n/en_US.ts @@ -273,5 +273,17 @@ refresh devices refresh devices + + install sndcpy + install sndcpy + + + start audio + start audio + + + stop audio + stop audio + diff --git a/QtScrcpy/res/i18n/zh_CN.qm b/QtScrcpy/res/i18n/zh_CN.qm index c033b31..801fd70 100644 Binary files a/QtScrcpy/res/i18n/zh_CN.qm and b/QtScrcpy/res/i18n/zh_CN.qm differ diff --git a/QtScrcpy/res/i18n/zh_CN.ts b/QtScrcpy/res/i18n/zh_CN.ts index 9eac260..43a45c5 100644 --- a/QtScrcpy/res/i18n/zh_CN.ts +++ b/QtScrcpy/res/i18n/zh_CN.ts @@ -273,5 +273,17 @@ refresh devices 刷新设备列表 + + install sndcpy + 安装sndcpy + + + start audio + 开始音频 + + + stop audio + 停止音频 + diff --git a/QtScrcpy/sndcpy/sndcpy.apk b/QtScrcpy/sndcpy/sndcpy.apk new file mode 100644 index 0000000..93e8cc2 Binary files /dev/null and b/QtScrcpy/sndcpy/sndcpy.apk differ diff --git a/QtScrcpy/sndcpy/sndcpy.bat b/QtScrcpy/sndcpy/sndcpy.bat new file mode 100644 index 0000000..f8a41a7 --- /dev/null +++ b/QtScrcpy/sndcpy/sndcpy.bat @@ -0,0 +1,53 @@ +@echo off + +echo Begin Runing... +set SNDCPY_PORT=28200 +set SNDCPY_APK=sndcpy.apk +set ADB=adb.exe + +if not "%1"=="" ( + set serial=-s %1 +) +if not "%2"=="" ( + set SNDCPY_PORT=%2 +) + +echo Waiting for device %1... +%ADB% %serial% wait-for-device || goto :error +echo Find device %1 + +for /f "delims=" %%i in ('%ADB% %serial% shell pm path com.rom1v.sndcpy') do set sndcpy_installed=%%i +if "%sndcpy_installed%"=="" ( + echo Install %SNDCPY_APK%... + %ADB% %serial% uninstall com.rom1v.sndcpy || goto :error + %ADB% %serial% install -t -r -g %SNDCPY_APK% || goto :error + echo Install %SNDCPY_APK% success +) + +echo Request PROJECT_MEDIA permission... +%ADB% %serial% shell appops set com.rom1v.sndcpy PROJECT_MEDIA allow + +echo Forward port %SNDCPY_PORT%... +%ADB% %serial% forward tcp:%SNDCPY_PORT% localabstract:sndcpy || goto :error + +echo Start %SNDCPY_APK%... +%ADB% %serial% shell am start com.rom1v.sndcpy/.MainActivity || goto :error + +:check_start +echo Waiting %SNDCPY_APK% start... +::timeout /T 1 /NOBREAK > nul +%ADB% %serial% shell sleep 0.1 +for /f "delims=" %%i in ("%ADB% shell 'ps | grep com.rom1v.sndcpy'") do set sndcpy_started=%%i +if "%sndcpy_started%"=="" ( + goto :check_start +) +echo %SNDCPY_APK% started... + +echo Ready playing... +::vlc.exe -Idummy --demux rawaud --network-caching=0 --play-and-exit tcp://localhost:%SNDCPY_PORT% +::ffplay.exe -nodisp -autoexit -probesize 32 -sync ext -f s16le -ar 48k -ac 2 tcp://localhost:%SNDCPY_PORT% +goto :EOF + +:error +echo Failed with error #%errorlevel%. +exit /b %errorlevel% diff --git a/QtScrcpy/ui/dialog.cpp b/QtScrcpy/ui/dialog.cpp index 733ffc1..35b1884 100644 --- a/QtScrcpy/ui/dialog.cpp +++ b/QtScrcpy/ui/dialog.cpp @@ -687,3 +687,27 @@ const QString &Dialog::getServerPath() } return serverPath; } + +void Dialog::on_startAudioBtn_clicked() +{ + if (ui->serialBox->count() == 0) { + qWarning() << "No device is connected!"; + return; + } + + m_audioOutput.start(ui->serialBox->currentText(), 28200); +} + +void Dialog::on_stopAudioBtn_clicked() +{ + m_audioOutput.stop(); +} + +void Dialog::on_installSndcpyBtn_clicked() +{ + if (ui->serialBox->count() == 0) { + qWarning() << "No device is connected!"; + return; + } + m_audioOutput.installonly(ui->serialBox->currentText(), 28200); +} diff --git a/QtScrcpy/ui/dialog.h b/QtScrcpy/ui/dialog.h index 5ef5a5f..a42142d 100644 --- a/QtScrcpy/ui/dialog.h +++ b/QtScrcpy/ui/dialog.h @@ -11,6 +11,7 @@ #include "adbprocess.h" #include "../QtScrcpyCore/include/QtScrcpyCore.h" +#include "audio/audiooutput.h" namespace Ui { @@ -57,6 +58,12 @@ private slots: void on_useSingleModeCheck_clicked(); void on_serialBox_currentIndexChanged(const QString &arg1); + void on_startAudioBtn_clicked(); + + void on_stopAudioBtn_clicked(); + + void on_installSndcpyBtn_clicked(); + private: bool checkAdbRun(); void initUI(); @@ -79,6 +86,7 @@ private: QMenu *m_menu; QAction *m_showWindow; QAction *m_quit; + AudioOutput m_audioOutput; }; #endif // DIALOG_H diff --git a/QtScrcpy/ui/dialog.ui b/QtScrcpy/ui/dialog.ui index 95b182e..c8f7683 100644 --- a/QtScrcpy/ui/dialog.ui +++ b/QtScrcpy/ui/dialog.ui @@ -7,7 +7,7 @@ 0 0 1293 - 419 + 454 @@ -941,6 +941,45 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + install sndcpy + + + + + + + start audio + + + + + + + stop audio + + + + + +