MediaPlayer在播放mp3时出现卡顿问题

33

我一直在尝试播放存储在原始资源中的mp3文件,但是当文件开始播放时,它会产生大约四分之一秒的声音,然后重新开始。(我知道这基本上是此处描述的问题的重复,但那里提供的解决方案对我没有起作用。)我尝试了几个方法,并在解决问题方面取得了一些进展,但问题并未完全解决。

以下是我设置播放文件的方法:

mPlayer.reset();
try {
    AssetFileDescriptor afd = getResources().openRawResourceFd(mAudioId);
    if (afd == null) {
        Toast.makeText(mOwner, "Could not load sound.",
                Toast.LENGTH_LONG).show();
        return;
    }
    mPlayer.setDataSource(afd.getFileDescriptor(),
            afd.getStartOffset(), afd.getLength());
    afd.close();
    mPlayer.prepare();
} catch (Exception e) {
    Log.d(LOG_TAG, "Could not load sound.", e);
    Toast.makeText(mOwner, "Could not load sound.", Toast.LENGTH_LONG)
            .show();
}

如果我退出活动(调用 mPlayer.release()),并回到它(创建一个新的MediaPlayer),则卡顿通常会消失,但不总是——只要我加载相同的声音文件。我尝试了几个没有任何区别的方法:

  • 将声音文件作为资源而不是资产加载。
  • 使用 MediaPlayer.create(getContext(), mAudioId) 来创建 MediaPlayer,并跳过对 setDataSource(...)prepare() 的调用。

然后我注意到,每当播放开始时,LogCat 总是显示此行:

DEBUG/AudioSink(37): bufferCount (4) is too small and increased to 12

我开始怀疑卡顿是由于明显的重新缓冲造成的。这促使我尝试了另一种方法:

  • 在调用prepare()后,调用mPlayer.start()并立即调用mPlayer.pause()

令我惊喜的是,这产生了很大的效果。很多卡顿现象都消失了,而且在这个过程中没有播放任何声音(至少在我听来是如此)。

然而,当我真正在调用mPlayer.start()时,它仍然会间歇性地出现卡顿。而且,这似乎是一个巨大的权宜之计。是否有任何办法可以完全、干净地解决这个问题?

编辑 更多信息;不确定是否相关。如果我在播放过程中调用pause(),然后寻找一个更早的位置,并再次调用start(),我会听到一个短暂的音频片段(约1/4秒),从暂停前的位置开始播放,然后才开始播放新位置的声音。这似乎表明存在更多的缓冲问题。

此外,卡顿(和暂停缓存)问题在1.6到3.0的模拟器上都会出现。


对于那些记忆力好的人来说,像这样调用 start()pause() 可能会让你想起我们以前在网页 applet 中强制预加载 AudioClip 所使用的技巧。 :-) - Ted Hopp
也许是声音文件的编码或长度问题。您尝试使用另一个声音文件来运行代码了吗? - Joseph Earl
@Joseph - 这是发生在四个文件上,长度从56秒到略超过4分钟不等。每个文件都是使用MPEG音频层1/2/3(mpga)的单个音频流,立体声,44,100hz采样率,128kb / s比特率。它们是使用GarageBand 5.1生成的。我尝试使用Audacity 1.3重新保存文件,但没有改变。这些文件在我的计算机上的媒体播放器中播放正常。 - Ted Hopp
MP3文件是否可以在默认的Android媒体播放器或其他Android媒体播放器应用程序中播放?如果可以,那么似乎您已经排除了文件的编码/比特率。如果不能,请尝试一些随Android附带的MP3文件。 - Joseph Earl
@Joseph - 当我将这些文件放在SD卡上并使用音乐应用程序播放它们时,它们可以正常播放。 - Ted Hopp
@TedHopp,你能帮我解决这个问题吗? - IceCream Sandwitch
2个回答

69
据我所知,MediaPlayer内部创建的缓冲区用于存储解压后的样本,而不是存储预取的压缩数据。我怀疑你的卡顿来自于I/O速度,因为它加载更多MP3数据进行解压缩。
最近我也遇到了类似的视频播放问题。由于 MediaPlayer 无法播放任意 InputStream (API非常奇怪),我想出的解决方案是编写一个小型的进程内Web服务器来通过HTTP提供本地文件(在SD卡上)。然后, MediaPlayer 通过形如http://127.0.0.1:8888/videofilename的URI加载它。
下面是我使用的StreamProxy类,用于将内容提供给MediaPlayer实例。基本用法是您实例化它,start()它,并使用类似 MediaPlayer.setDataSource("http://127.0.0.1:8888/localfilepath");的方式设置您的媒体播放器。需要注意的是,它相当试验性,并且可能并不完全没有错误。它是为了解决与您类似的问题而编写的,即MediaPlayer无法播放正在下载的文件。以这种方式本地流式传输文件可以解决这个限制(即我有一个线程正在下载文件,而StreamProxy将其提供给mediaplayer)。
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import android.os.AsyncTask;
import android.os.Looper;
import android.util.Log;

public class StreamProxy implements Runnable {

    private static final int SERVER_PORT=8888;

    private Thread thread;
    private boolean isRunning;
    private ServerSocket socket;
    private int port;

    public StreamProxy() {

        // Create listening socket
        try {
          socket = new ServerSocket(SERVER_PORT, 0, InetAddress.getByAddress(new byte[] {127,0,0,1}));
          socket.setSoTimeout(5000);
          port = socket.getLocalPort();
        } catch (UnknownHostException e) { // impossible
        } catch (IOException e) {
          Log.e(TAG, "IOException initializing server", e);
        }

    }

    public void start() {
        thread = new Thread(this);
        thread.start();
    }

    public void stop() {
        isRunning = false;
        thread.interrupt();
        try {
            thread.join(5000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
    }

    @Override
      public void run() {
        Looper.prepare();
        isRunning = true;
        while (isRunning) {
          try {
            Socket client = socket.accept();
            if (client == null) {
              continue;
            }
            Log.d(TAG, "client connected");

            StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client);
            if (task.processRequest()) {
                task.execute();
            }

          } catch (SocketTimeoutException e) {
            // Do nothing
          } catch (IOException e) {
            Log.e(TAG, "Error connecting to client", e);
          }
        }
        Log.d(TAG, "Proxy interrupted. Shutting down.");
      }




    private class StreamToMediaPlayerTask extends AsyncTask<String, Void, Integer> {

        String localPath;
        Socket client;
        int cbSkip;

        public StreamToMediaPlayerTask(Socket client) {
            this.client = client;
        }

        public boolean processRequest() {
            // Read HTTP headers
            String headers = "";
            try {
              headers = Utils.readTextStreamAvailable(client.getInputStream());
            } catch (IOException e) {
              Log.e(TAG, "Error reading HTTP request header from stream:", e);
              return false;
            }

            // Get the important bits from the headers
            String[] headerLines = headers.split("\n");
            String urlLine = headerLines[0];
            if (!urlLine.startsWith("GET ")) {
                Log.e(TAG, "Only GET is supported");
                return false;               
            }
            urlLine = urlLine.substring(4);
            int charPos = urlLine.indexOf(' ');
            if (charPos != -1) {
                urlLine = urlLine.substring(1, charPos);
            }
            localPath = urlLine;

            // See if there's a "Range:" header
            for (int i=0 ; i<headerLines.length ; i++) {
                String headerLine = headerLines[i];
                if (headerLine.startsWith("Range: bytes=")) {
                    headerLine = headerLine.substring(13);
                    charPos = headerLine.indexOf('-');
                    if (charPos>0) {
                        headerLine = headerLine.substring(0,charPos);
                    }
                    cbSkip = Integer.parseInt(headerLine);
                }
            }
            return true;
        }

        @Override
        protected Integer doInBackground(String... params) {

                        long fileSize = GET CONTENT LENGTH HERE;

            // Create HTTP header
            String headers = "HTTP/1.0 200 OK\r\n";
            headers += "Content-Type: " + MIME TYPE HERE + "\r\n";
            headers += "Content-Length: " + fileSize  + "\r\n";
            headers += "Connection: close\r\n";
            headers += "\r\n";

            // Begin with HTTP header
            int fc = 0;
            long cbToSend = fileSize - cbSkip;
            OutputStream output = null;
            byte[] buff = new byte[64 * 1024];
            try {
                output = new BufferedOutputStream(client.getOutputStream(), 32*1024);                           
                output.write(headers.getBytes());

                // Loop as long as there's stuff to send
                while (isRunning && cbToSend>0 && !client.isClosed()) {

                    // See if there's more to send
                    File file = new File(localPath);
                    fc++;
                    int cbSentThisBatch = 0;
                    if (file.exists()) {
                        FileInputStream input = new FileInputStream(file);
                        input.skip(cbSkip);
                        int cbToSendThisBatch = input.available();
                        while (cbToSendThisBatch > 0) {
                            int cbToRead = Math.min(cbToSendThisBatch, buff.length);
                            int cbRead = input.read(buff, 0, cbToRead);
                            if (cbRead == -1) {
                                break;
                            }
                            cbToSendThisBatch -= cbRead;
                            cbToSend -= cbRead;
                            output.write(buff, 0, cbRead);
                            output.flush();
                            cbSkip += cbRead;
                            cbSentThisBatch += cbRead;
                        }
                        input.close();
                    }

                    // If we did nothing this batch, block for a second
                    if (cbSentThisBatch == 0) {
                        Log.d(TAG, "Blocking until more data appears");
                        Thread.sleep(1000);
                    }
                }
            }
            catch (SocketException socketException) {
                Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
            }
            catch (Exception e) {
                Log.e(TAG, "Exception thrown from streaming task:");
                Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
                e.printStackTrace();                
            }

            // Cleanup
            try {
                if (output != null) {
                    output.close();
                }
                client.close();
            }
            catch (IOException e) {
                Log.e(TAG, "IOException while cleaning up streaming task:");                
                Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
                e.printStackTrace();                
            }

            return 1;
        }

    }
}

1
我并不是在建议你完全不使用压缩数据,而是建议你将部分或全部压缩数据预加载至内存中。我认为你的问题在于,当你仅给MediaPlayer一个文件描述符时,它没有缓冲足够的数据来输入MP3解压器…这会导致缓冲区欠载,从而出现卡顿。 - Reuben Scratton
1
啊,这就解释了为什么在开始时调用start()然后立即调用pause()会有影响。我猜测它并不完全有效,因为调用pause()的时间太早了,所以缓冲区没有机会填满。我用seekTo(500)替换了start/pause序列(并稍微调整了onSeekComplete),现在卡顿似乎已经消失了! - Ted Hopp
2
无法这样做,因为我不再使用这种hacky技术。现在我完全避免使用MediaPlayer,因为它充满了错误。我用一个强大的自定义Java包装器替换了它,这个包装器是FFmpeg的一个端口,可以播放任意InputStreams,还有其他功能。 - Reuben Scratton
你好 @ReubenScratton,你有没有在Android手机上通过RTP将音频文件流式传输到同一网络中的其他设备方面有任何经验?我正在尝试这样做,但我只能听到静音:http://stackoverflow.com/questions/18257438/android-send-wav-to-sip-phone-via-rtp-g-711-pcmu-very-noisy-crackling-sound - B770
@ReubenScratton 你好,我正在尝试实现Http流方案,请问您能否分享一下您的代码吗?非常感谢。 - anz
显示剩余24条评论

3
您是否更适合使用prepareAsync并响应setOnPreparedListener?根据您的活动工作流程,当MediaPlayer首次初始化时,您可以设置准备监听器,然后在实际加载资源时稍后调用mPlayer.prepareAsync(),然后在那里开始播放。我使用类似的东西,尽管是针对基于网络的流媒体资源:
MediaPlayer m_player;
private ProgressDialog m_progressDialog = null;

...

try {
    if (m_player != null) {
    m_player.reset();
    } else {
    m_player = new MediaPlayer();
    }

    m_progressDialog = ProgressDialog
        .show(this,
            getString(R.string.progress_dialog_please_wait),
            getString(R.string.progress_dialog_buffering),
            true);

    m_player.setOnPreparedListener(this);
    m_player.setAudioStreamType(AudioManager.STREAM_MUSIC);
    m_player.setDataSource(someSource);
    m_player.prepareAsync();
} catch (Exception ex) {
}

...

public void onPrepared(MediaPlayer mp) {
    if (m_progressDialog != null && m_progressDialog.isShowing()) {
      m_progressDialog.dismiss();
    }

    m_player.start();
}

显然,一个完整的解决方案还有很多需要考虑(例如错误处理等),但我认为这应该是一个很好的起点示例,你可以在此基础上提取流程。


我会尝试这个并报告效果。我对此并不抱太大希望,因为问题似乎不在准备时间上;它似乎是在start()被调用后的处理中发生的。但是,我首先要承认我并不完全理解MediaPlayer内部的工作方式。 - Ted Hopp
3
我现在尝试了这种变化,但对问题没有任何影响。 - Ted Hopp
你能分享一下正在播放的文件的详细信息吗?比特率是多少?用什么编码器构建了MP3容器?我知道这只是一个解决方法,但也许你期望太高了,设备难以推送数据,因此降低比特率可能会解决你的问题。另外,这是在模拟器中吗?问题是否发生在不同的设备上? - Matt Bishop
@Matt - 一共涉及到四个文件。它们是由GarageBand 5.1创建的,采样率为44,100hz。其中三个文件的比特率为128kbs,大约每个文件长度为一分钟。第四个文件大约有4分钟长,比特率为86kbs。这些文件在初始卡顿(实际上更像是重新启动)后可以平稳播放,因此我想知道比特率是否可能是问题所在。 - Ted Hopp
我尝试查找 Android MP3 播放的源代码,但一直未能成功。有些编码器并不总是以最高效的方式创建容器,这可能导致 Android 的 MP3 播放模块卡顿。或许你可以尝试将声音文件导出为纯 PCM,然后使用 LAME 等其他编码器,看看是否会产生任何播放差异。你还可以在音轨中填充半秒钟的静音,这样就不会听到杂音了。我知道这些方法只是权宜之计,但目前似乎无法更改 MediaPlayer;缓冲区也无法调整。 - Matt Bishop
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接