最近入手了一个JBL回音壁,通过电视HDMI ARC接口连接音箱HDMI ARC接口
但后面发现一个问题,如果开着电视,暂停观看10分钟以上(音响10分钟无声音输入)就会自动进入休眠,此时只能使用遥控手动再打开一次音响才能恢复,这导致非常麻烦,本来使用ARC以为已经可以丢弃音响遥控器了
网上看到有人电脑使用回音壁,写了一个脚本让电脑每隔9分钟播放一个高频声音(人耳无法察觉)
但电视TV就很麻烦,海信电视还没法root无法自动运行python
于是打算写个安卓TV APP来做到自启并自动播放音频
先用AU制作一个长达9分钟的mp3音频,中间穿插两秒高频声音(18600Hz)
然后使用Android Studio写一个min SDK Version为23的Android TV APP(电视系统为Android 7.0)
参考此源码小做修改,修改自启动,无UI并且启动自动开始无限循环播放音频
Android实现后台播放音乐(附带源码)_android apk 后台播放音乐-CSDN博客
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature android:name="android.software.leanback" android:required="true" /> <application android:allowBackup="true" android:extractNativeLibs="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@android:style/Theme.Translucent.NoTitleBar"> <receiver android:name=".MyReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MEDIA_MOUNTED"/> <action android:name="android.intent.action.MEDIA_UNMOUNTED"/> <action android:name="android.intent.action.MEDIA_EJECT"/> <action android:name="android.intent.action.MEDIA_REMOVED"/> <data android:scheme="file" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> <activity android:name=".MainActivity" android:banner="@drawable/app_icon_your_company" android:exported="true" android:icon="@drawable/app_icon_your_company" android:label="@string/app_name" android:logo="@drawable/app_icon_your_company" android:screenOrientation="landscape"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> </intent-filter> </activity> <!-- 后台播放服务 --> <service android:name=".MusicService" android:exported="false" android:foregroundServiceType="mediaPlayback" /> <activity android:name=".DetailsActivity" android:exported="false" /> <activity android:name=".PlaybackActivity" android:exported="false" /> <activity android:name=".BrowseErrorActivity" android:exported="false" /> </application> </manifest>
MainAcitvity.java
package com.example.backgroundmusic;
import android.content.Intent;
import android.os.Bundle;
import android.os.Build;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentActivity;
/*
* Main Activity class that loads {@link MainFragment}.
*/
public class MainActivity extends FragmentActivity {
private Button btnStart, btnStop;
@Override
protected void onStart() {
super.onStart();
}
@Override
protected void onResume() {//进入前台
super.onResume();
moveTaskToBack(isFinishing());//从前台唤醒立即进入后台运行
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
moveTaskToBack(isFinishing());//立即进入后台运行
// setContentView(R.layout.activity_main); // 加载布局
Intent autoStartIntent = new Intent(this, MusicService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(autoStartIntent);
} else {
startService(autoStartIntent);
}
//
//
// btnStart = findViewById(R.id.btn_start);
// btnStop = findViewById(R.id.btn_stop);
//
// // 点击“开始播放”,启动前台服务
// btnStart.setOnClickListener(v -> {
// Intent intent = new Intent(this, MusicService.class);
// // API26+ 要用 startForegroundService
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// startForegroundService(intent);
// } else {
// startService(intent);
// }
// });
//
// // 点击“停止播放”,停止服务
// btnStop.setOnClickListener(v -> {
// Intent intent = new Intent(this, MusicService.class);
// stopService(intent);
// });
}
}MyReceiver.java
package com.example.backgroundmusic;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.example.backgroundmusic.MainActivity;
public class MyReceiver extends BroadcastReceiver {
private final String ACTION_BOOT = "android.intent.action.BOOT_COMPLETED";
private final String ACTION_MEDIA_MOUNTED = "android.intent.action.MEDIA_MOUNTED";
private final String ACTION_MEDIA_UNMOUNTED = "android.intent.action.MEDIA_UNMOUNTED";
private final String ACTION_MEDIA_EJECT = "android.intent.action.MEDIA_EJECT";
private final String ACTION_MEDIA_REMOVED = "android.intent.action.MEDIA_REMOVED";
@Override
public void onReceive(Context context, Intent intent) {
// 判断是否是系统开启启动的消息,如果是,则启动APP
if ( ACTION_BOOT.equals(intent.getAction()) ||
ACTION_MEDIA_MOUNTED.equals(intent.getAction()) ||
ACTION_MEDIA_UNMOUNTED.equals(intent.getAction()) ||
ACTION_MEDIA_EJECT.equals(intent.getAction()) ||
ACTION_MEDIA_REMOVED.equals(intent.getAction())
) {
Intent intentMainActivity = new Intent(context, MainActivity.class);
intentMainActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intentMainActivity);
}
}
}MusicService.java
package com.example.backgroundmusic;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
/**
* MusicService:在前台服务中使用 MediaPlayer 播放音乐
*/
public class MusicService extends Service {
// 通知通道与通知 ID
private static final String CHANNEL_ID = "music_play_channel";
private static final int NOTIF_ID = 1001;
// 通知动作字符串,用于区分点击事件
private static final String ACTION_PLAY = "ACTION_PLAY";
private static final String ACTION_PAUSE = "ACTION_PAUSE";
private static final String ACTION_STOP = "ACTION_STOP";
private MediaPlayer mediaPlayer;
private boolean isPlaying = false;
@Override
public void onCreate() {
super.onCreate();
// 1. 创建通知渠道(Android 8.0+)
createNotificationChannel();
// 2. 初始化 MediaPlayer
initMediaPlayer();
}
/**
* 在 startService/startForegroundService 后回调
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 区分点击通知的 Action
if (intent != null && intent.getAction() != null) {
switch (intent.getAction()) {
case ACTION_PLAY:
resumeMusic();
break;
case ACTION_PAUSE:
pauseMusic();
break;
case ACTION_STOP:
stopSelf(); // 停止 Service
return START_NOT_STICKY;
}
} else {
// 首次启动,直接开始播放
startMusic();
}
// 每次 onStartCommand 都需要调用 startForeground,保持前台状态
startForeground(NOTIF_ID, buildNotification());
// 如果被系统杀掉,不再自动重启
return START_NOT_STICKY;
}
/**
* 初始化 MediaPlayer,并设置音频属性
*/
private void initMediaPlayer() {
mediaPlayer = MediaPlayer.create(this, R.raw.sample_music);
mediaPlayer.setLooping(true); // 循环播放
// 适配 API21+ 的音频属性
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mediaPlayer.setAudioAttributes(
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
);
} else {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
}
/** 开始播放并更新状态 */
private void startMusic() {
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
isPlaying = true;
}
}
/** 暂停播放并更新状态 */
private void pauseMusic() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
isPlaying = false;
}
}
/** 恢复播放并更新状态 */
private void resumeMusic() {
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
isPlaying = true;
}
}
/** 构建前台服务通知,包含播放/暂停/停止按钮 */
private Notification buildNotification() {
// 点击通知主体,返回 MainActivity
PendingIntent mainIntent = PendingIntent.getActivity(
this, 0,
new Intent(this, MainActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 播放/暂停按钮 Intent
Intent playPauseIntent = new Intent(this, MusicService.class)
.setAction(isPlaying ? ACTION_PAUSE : ACTION_PLAY);
PendingIntent ppPending = PendingIntent.getService(
this, 1, playPauseIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 停止按钮 Intent
Intent stopIntent = new Intent(this, MusicService.class)
.setAction(ACTION_STOP);
PendingIntent stopPending = PendingIntent.getService(
this, 2, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 构造 Notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在播放音乐")
.setContentText(isPlaying ? "点击暂停" : "点击播放")
.setSmallIcon(R.drawable.ic_music_note)
.setContentIntent(mainIntent) // 点击通知主体
.addAction(isPlaying ? R.drawable.ic_pause : R.drawable.ic_play,
isPlaying ? "暂停" : "播放", ppPending)
.addAction(R.drawable.ic_stop, "停止", stopPending)
// 使用 MediaStyle,支持在锁屏及车载展示
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0, 1));
return builder.build();
}
/** 创建通知渠道(Android 8.0+) */
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(
CHANNEL_ID,
"音乐播放服务",
NotificationManager.IMPORTANCE_LOW
);
chan.setDescription("用于在后台播放音乐的前台服务");
NotificationManager mgr = getSystemService(NotificationManager.class);
if (mgr != null) {
mgr.createNotificationChannel(chan);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
// 释放 MediaPlayer
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
// 取消前台状态与通知
stopForeground(true);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
// 不提供绑定,此处返回 null
return null;
}
}activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main_browse_fragment" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="24dp" tools:context=".MainActivity" tools:deviceIds="tv" tools:ignore="MergeRootFrame"> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="开始播放音乐" android:layout_gravity="center" /> <Button android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="停止播放音乐" android:layout_gravity="center" android:layout_marginTop="60dp"/> </FrameLayout>
build.gradle
plugins {
alias(libs.plugins.android.application)
}
android {
namespace 'com.example.backgroundmusic'
compileSdk {
version = release(36)
}
defaultConfig {
applicationId "com.example.backgroundmusic"
minSdk 23
targetSdk 36
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation(platform(libs.kotlin.bom))
implementation libs.androidx.leanback
implementation libs.glide
implementation libs.androidx.appcompat
}根据网页教程放入资源文件之后,生成APK并通过ADB安装
发现无法做到自启(可能是国产电视系统限制,拦截自启)
家里有一台装了armbian的用作HAOS的机顶盒,于是想到用服务+sh脚本循环执行的方式来持续通过ADB指令来启动电视上的APP,只要电视开机就会启动APP开始播放音频
写了 一键生成启动服务/一键停止卸载服务 的sh脚本
一键生成启动服务脚本
#!/usr/bin/env/ bash
set -e
echo "=== 安装 adb-keepalive 服务开始 ==="
# 1. 确保 adb 已安装
if ! command -v adb >/dev/null 2>&1; then
echo "ADB 未检测到,正在安装 android-tools-adb..."
apt update -y && apt install -y android-tools-adb
else
echo "ADB 已存在,跳过安装。"
fi
# 2. 创建脚本文件
echo "创建 /usr/local/bin/adb-keepalive.sh ..."
cat >/usr/local/bin/adb-keepalive.sh <<"EOF"
#!/usr/bin/env bash
# adb-keepalive.sh - 稳定保持与电视 ADB 连接并启动应用
# 支持设备重启自动重连 + 日志每日轮换 + 保留两天日志
# ==== 配置 ====
ADB_TARGET="192.168.31.147:5555" # 目标设备,带端口号
AM_ACTIVITY="com.example.backgroundmusic/.MainActivity" # 要启动的 Activity
LOGDIR="/var/log/adb-keepalive" # 日志目录
CONNECT_TIMEOUT=12 # adb connect 超时(秒)
SLEEP_INTERVAL=480 # 循环间隔(秒)
# ==== 初始化 ====
mkdir -p "$LOGDIR"
ADB_BIN="$(command -v adb || true)"
TIMEOUT_BIN="$(command -v timeout || true)"
# 捕获退出信号
trap 'echo "$(date +'%F %T') adb-keepalive.sh received signal, exiting." >> "$LOGFILE"; exit 0' TERM INT
echo "$(date +'%F %T') adb-keepalive.sh started (target: $ADB_TARGET)" >> "$LOGDIR/adb-keepalive-$(date +%F).log"
# ==== 主循环 ====
while true; do
TODAY=$(date +%F)
LOGFILE="$LOGDIR/adb-keepalive-${TODAY}.log"
# 清理旧日志,只保留今天和昨天
find "$LOGDIR" -type f -name "adb-keepalive-*.log" \
! -name "adb-keepalive-${TODAY}.log" \
! -name "adb-keepalive-$(date -d 'yesterday' +%F).log" \
-delete 2>/dev/null
echo "$(date +'%F %T') ===== Loop start =====" >> "$LOGFILE"
if [ -n "$ADB_BIN" ]; then
# 检查设备状态
STATE=$($ADB_BIN get-state 2>/dev/null || echo "offline")
if [ "$STATE" != "device" ]; then
echo "$(date +'%F %T') Device offline or not ready, reconnecting..." >> "$LOGFILE"
$ADB_BIN disconnect "$ADB_TARGET" >> "$LOGFILE" 2>&1 || true
if [ -n "$TIMEOUT_BIN" ]; then
$TIMEOUT_BIN ${CONNECT_TIMEOUT}s $ADB_BIN connect "$ADB_TARGET" >> "$LOGFILE" 2>&1 || {
echo "$(date +'%F %T') adb connect failed or timed out" >> "$LOGFILE"
}
else
$ADB_BIN connect "$ADB_TARGET" >> "$LOGFILE" 2>&1 || {
echo "$(date +'%F %T') adb connect failed" >> "$LOGFILE"
}
fi
fi
# 尝试启动应用
$ADB_BIN shell am start -n "$AM_ACTIVITY" >> "$LOGFILE" 2>&1 || {
echo "$(date +'%F %T') adb shell am start failed" >> "$LOGFILE"
}
else
echo "$(date +'%F %T') adb binary not found; skipping adb commands" >> "$LOGFILE"
fi
echo "$(date +'%F %T') ===== Loop end (sleep ${SLEEP_INTERVAL}s) =====" >> "$LOGFILE"
sleep $SLEEP_INTERVAL
done
EOF
chmod +x /usr/local/bin/adb-keepalive.sh
mkdir -p /var/log/adb-keepalive
chmod 755 /var/log/adb-keepalive
# 3. 创建 systemd 服务文件
echo "创建 /etc/systemd/system/adb-keepalive.service ..."
cat >/etc/systemd/system/adb-keepalive.service <<"EOF"
[Unit]
Description=ADB Keepalive Service - keeps device connected and starts background music app
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/adb-keepalive.sh
User=root
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# 4. 启用并启动服务
echo "启用并启动 adb-keepalive.service ..."
systemctl daemon-reload
systemctl enable --now adb-keepalive.service
echo "=== adb-keepalive 服务已安装并运行 ==="
echo
echo "查看服务状态: sudo systemctl status adb-keepalive.service"
echo "查看实时日志: sudo tail -f /var/log/adb-keepalive/adb-keepalive-$(date +%F).log"
echo一键停止卸载服务脚本
#!/usr/bin/env bash set -e echo "=== 开始卸载 adb-keepalive 服务 ===" SERVICE_NAME="adb-keepalive.service" SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME" SCRIPT_FILE="/usr/local/bin/adb-keepalive.sh" LOG_DIR="/var/log/adb-keepalive" # 1. 检查服务是否在运行 if systemctl list-units --type=service | grep -q "$SERVICE_NAME"; then echo "强制停止服务..." # 先用 systemctl stop 尝试 systemctl stop "$SERVICE_NAME" || true # 找到所有该服务的主进程并杀掉(防止卡住) PIDS=$(systemctl show -p MainPID --value "$SERVICE_NAME") if [ -n "$PIDS" ] && [ "$PIDS" != "0" ]; then echo "杀掉残留进程: $PIDS" kill -9 $PIDS || true fi sleep 1 systemctl reset-failed "$SERVICE_NAME" || true else echo "服务未运行,跳过停止。" fi # 2. 禁用服务 if systemctl list-unit-files | grep -q "$SERVICE_NAME"; then echo "禁用服务..." systemctl disable "$SERVICE_NAME" || true fi # 3. 删除 systemd 服务文件 if [ -f "$SERVICE_FILE" ]; then echo "删除 systemd 服务文件: $SERVICE_FILE" rm -f "$SERVICE_FILE" fi # 4. 删除脚本文件 if [ -f "$SCRIPT_FILE" ]; then echo "删除脚本文件: $SCRIPT_FILE" rm -f "$SCRIPT_FILE" fi # 5. 删除日志目录 if [ -d "$LOG_DIR" ]; then echo "删除日志目录: $LOG_DIR" rm -rf "$LOG_DIR" fi # 6. 重新加载 systemd 配置 systemctl daemon-reload echo "=== adb-keepalive 服务与文件已完全卸载 ==="
注意里面的ip地址要改成电视的,脚本会自动清理日志,循环时间也可自行修改
至此经测试音响已经不会自动休眠,并且播放的音频无感知,完美解决