首页 Android 正文

【Android】安卓TV后台播放定时高频声音阻止JBL回音壁进入休眠

方雪墨头像 方雪墨 Android 2025-11-09 22:11:49 0 26

最近入手了一个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地址要改成电视的,脚本会自动清理日志,循环时间也可自行修改

至此经测试音响已经不会自动休眠,并且播放的音频无感知,完美解决

本文地址:https://blog.treecyan.com/?id=10
若非特殊说明,文章均属本站原创,转载请注明原链接。

欢迎 发表评论:

网站分类

标签列表

退出请按Esc键