JavaFX+WebView音乐播放器

时间:2024-04-13 12:08:16

先附上我实现的JavaFX音乐播放器的下载地址和码云地址。

下载地址(懒人需花费1CB,并且还存在一些缺陷,在码云中都修复了):https://download.****.net/download/xss13/10691621

码云地址:https://gitee.com/com_shisan/MyUtils

爬虫地址:https://gitee.com/com_shisan/music-lib

JavaFX+WebView音乐播放器

基于JavaFX+WebView实现的音乐播放器,这里主要谈谈遇到的一些坑。

  • WebView的缺陷
  • load 本地资源的解决方案
  • 没找到一篇相关的JAudioTagger对MP3文件的完整操作的文章
  • JavaMod并没有一篇成型的API文档,虽然官网说 how to use javamod as api?
  • 想要支持全格式但还要兼顾电平脉冲(Javafx自带的脉冲效果很水),JavaMod不支持aac,mp4,m4a等格式

WebView的缺陷

WebView实际上是有很多坑和bug等着你去踩的,当初遇到的第一个问题是程序点击关闭按钮后仍然在后台运行,经过使用jconsole等工具观察,发现是WebView页面存在着gif动图,在关闭时该播放线程没有被销毁导致的,我在onCloseRequest的时候加上了webEngine.loadContent("")才解决了这个问题。除此之外,有时候css样式并不能完全支持,有些瑕疵,最坑的是如果你希望在WebView中使用h5的最新且在试验阶段的API,那就别想了,更何况AudioContext这样的对象都不存在。更多坑等你自己发觉。

为了简化webview与java通讯,本人也是费了很大周折最后决定使用了json+注解的方式传输,依赖了阿里的fastjson和jdk.internal.org.objectweb.asm包下的类扫描工具(话说这些工具类好像在JDK9及之后版本就访问不到了)。

load 本地资源的解决方案

作为一个内嵌在Java内部的浏览器,怎么会不支持load local source?与常规浏览器一样会报not allow load local source的错误,经过在*上一番折腾,终于发现一个可以有效解决次问题的解决方案:自定义protocol。

public class ClassPathURLStreamHandlerFactory implements URLStreamHandlerFactory {
    @Override
    public URLStreamHandler createURLStreamHandler(String protocol) {
        if("classpath".equals(protocol)){
            return new URLStreamHandler() {
                @Override
                protected URLConnection openConnection(URL u) throws IOException {
                    try{
                        String path = u.getPath();
                        if(path.startsWith("/")) path = path.substring(1);
                        URL url = (Scanner.getUrlLoader()==null?getClass().getClassLoader():Scanner.getUrlLoader()).getResource(path);
                        return url.openConnection();
                    }catch (IOException e){
                        throw e;
                    }catch (Exception e){

                    }
                    return null;
                }
            };
        }
        return null;
    }
}

然后在你的程序启动类上加个static块注册以一就OK了,如果你希望读取local资源你可以自定义URLConnection就可以解决这个额问题。

static{
        URL.setURLStreamHandlerFactory(new ClassPathURLStreamHandlerFactory());
    }

没找到一篇相关的JAudioTagger对MP3文件的完整操作的文章

JAudioTagger无疑是一个相当不错的Java版本的音频文件标签信息解析工具,它不光支持读取还支持修改,只是不知道为什么网上的文章里没有一个相关的修改操作的文章。早在14年之前我就尝试用它获取过mp3文件信息,只是当时水平有限,既不会读既有的api和demo,且debugger源码的功底也不深厚再加上当时对这个需求不是很强烈,导致一直使用的是之前从网上copy出来的一段很不规范的代码来读取MP3音频文件。这次对源码的读和写都做了较深入的debugger跟踪,加上查看了官网给的文档,实现这个功能并不难。

先说明以下JAudioTagger自带了对字符串编码猜测功能,只不过在某一行有个小小的bug,我对源码做了小小修改(加上之前一个****哥们写的内容,时间太久远记不起来是谁了),实现了自动猜测字符串编码功能,详见我的 com.music.player模块内的com.music.util.ReadMp3Info类的guessAndCovertText方法。

//获取Mp3音频标签
try {
                MP3File mp3F = new MP3File(path);
                AbstractID3v2Tag d32 = mp3F.getID3v2Tag();
                if (d32 != null) {
                    title = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_TITLE));
                    author = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_ARTIST));
                    special = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_ALBUM));
                    date = guessAndConvertText(d32.getFirst(ID3v24Frames.FRAME_ID_YEAR));
                    if(StringUtil.isNull(date)){
                        date = guessAndConvertText(d32.getFirst(ID3v23Frames.FRAME_ID_V3_TYER));
                    }
                    node = guessAndConvertText(d32.getFirst(ID3v23Frames.FRAME_ID_V3_UNSYNC_LYRICS));
                    AbstractID3v2Frame imgData = d32.getFirstField("APIC");
                    if(imgData != null){
                        AbstractTagFrameBody imgDataBody = imgData.getBody();
                        if(imgDataBody instanceof FrameBodyAPIC){
                            picByte = ((FrameBodyAPIC) imgDataBody).getImageData();
                            picBase64 = Base64.getEncoder().encodeToString(picByte);
                            String picType = checkImage(picByte);
                            if(picType!=null) this.picType = picType.toLowerCase();
                        }
                    }
                }
                ID3v1Tag d31 = mp3F.getID3v1Tag();
                if (d31 != null) {
                    if (StringUtil.isNull(title)) {
                        title = guessAndConvertText(d31.getFirstTitle());
                    }
                    if (StringUtil.isNull(author)) {
                        author = guessAndConvertText(d31.getFirstArtist());
                    }
                    if (StringUtil.isNull(special)) {
                        special = guessAndConvertText(d31.getFirstAlbum());
                    }
                    if (StringUtil.isNull(date)) {
                        date = d31.getFirstYear();
                        if (StringUtil.isNull(date)) {
                            date = "";
                        }
                    }
                }
            } catch (Exception ex) {
            }

对于MP3标签的修改,是读取的逆向过程,这里又遇到了一个坑,那就是AbstractID3v2Frame类的setContent方法。刚开始我以为对title等普通信息直接setContent就可以了,但是总是报相同的错误,查明原因是:

/**
     * Sets the content of the field.
     *
     * @param content fields content.
     */
    public void setContent(String content)
    {
        throw new UnsupportedOperationException("Not implemented please use the generic tag methods for setting content");
    }

所有的实现类中没有一个类实现了该方法!所以我们只能调用setBody方法来实现信息的写入了:

Map<String,Object> otherInfo = musicInfo.getOtherInfo();
        ImageUtil.handleMp3InfoPic(otherInfo,cacheDir);
        String title = otherInfo==null?
                musicInfo.getName():(String)otherInfo.getOrDefault("displayName",musicInfo.getName());

        //将专辑等信息写入到mp3
        MP3File mp3File = new MP3File(targetFile);
        AbstractID3v2Tag id3v2Tag = mp3File.getID3v2Tag();
        if(id3v2Tag == null){
            id3v2Tag = new ID3v23Tag();
        }
        AbstractTagFrameBody body;
        //title 标题
        AbstractID3v2Frame abstractID3v2Frame = new ID3v24Frame(ID3v24Frames.FRAME_ID_TITLE);
        abstractID3v2Frame.setEncoding("UTF-8");
        body = new FrameBodyTIT2(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),title);
        abstractID3v2Frame.setBody(body);
        id3v2Tag.setFrame(abstractID3v2Frame);
        //artist 艺术家(本应用称作作者)
        String info;
        info = (String) otherInfo.get("author");
        if(StringUtil.isNotNull(info)){
            abstractID3v2Frame = new ID3v24Frame(ID3v24Frames.FRAME_ID_ARTIST);
            abstractID3v2Frame.setEncoding("UTF-8");
            body = new FrameBodyTPE1(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),info);
            abstractID3v2Frame.setBody(body);
            id3v2Tag.setFrame(abstractID3v2Frame);
        }
        //album 专辑
        info = (String) otherInfo.get("albumName");
        if(StringUtil.isNotNull(info)){
            abstractID3v2Frame = new ID3v24Frame(ID3v24Frames.FRAME_ID_ALBUM);
            abstractID3v2Frame.setEncoding("UTF-8");
            body = new FrameBodyTALB(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),info);
            abstractID3v2Frame.setBody(body);
            id3v2Tag.setFrame(abstractID3v2Frame);
        }
        //pic 图片
        info = (String) otherInfo.get("img");
        if(StringUtil.isNotNull(info)){
            abstractID3v2Frame = new ID3v24Frame("APIC");
            FrameBodyAPIC picBody = new FrameBodyAPIC();
            picBody.setPictureType((byte)1);
            picBody.setTextEncoding(TextEncoding.getInstanceOf().getIdForValue("ISO-8859-1").byteValue());
            //data:image/jpeg;base64,xxxxx
            picBody.setMimeType(info.substring(info.indexOf(":")+1,info.indexOf(";")));
            picBody.setImageData(Base64.getDecoder().decode(info.substring(info.indexOf(",")+1)));
            picBody.setDescription("");
            abstractID3v2Frame.setBody(picBody);
            id3v2Tag.setFrame(abstractID3v2Frame);
        }
        //lrc 歌词
        if(readLRC!=null && StringUtil.isNotNull(readLRC.getLrcStr())){
            String lrc = readLRC.getLrcStr();
            abstractID3v2Frame = new ID3v23Frame(ID3v23Frames.FRAME_ID_V3_UNSYNC_LYRICS);
            abstractID3v2Frame.setEncoding("UTF-8");
            body = new FrameBodyUSLT(TextEncoding.getInstanceOf().getIdForValue("UTF-8").byteValue(),"","",lrc);
            abstractID3v2Frame.setBody(body);
            id3v2Tag.setFrame(abstractID3v2Frame);
        }

        mp3File.setID3v2Tag(id3v2Tag);
//save()与commit()做相同的事情,commit()调用了save()
        mp3File.commit();

这里还有两个坑:

  1. 不要自以为是的把某个属性设置为null,不然会报空指针异常让你摸不着头脑,你可以使用空字符串替代
  2. 每一种信息都对应了自己的AbstractID3v2Frame实现类,这里给个参考吧:

支持的编码有四种,自己体会:

JavaFX+WebView音乐播放器

 自行查看AbstractID3v2Frame的所有实现类看有哪些需要自己去修改的,如果头晕对不上,就去官网看文档,这里给个截图和文档传送门:(写这片博客时网页改版了)

JavaFX+WebView音乐播放器http://www.jthink.net/jaudiotagger/tagmapping.html

JavaMod并没有一篇成型的API文档,虽然官网说 how to use javamod as api?

先上一张JavaMod图:

JavaFX+WebView音乐播放器

着是国外一个大佬从2006年起开始做的音乐播放器,JavaFX+WebView音乐播放器

没有API相关文档,Java版本的用Swing实现,效果还不错,给个传送门:http://www.javamod.de/

这里我对他的部分代码做了简单的抽取就直接拿来用了,他的软件还是有个小bug,就是你设置的volumn和blance每次播放心下一首都会被清0,你还得重新去调,就是音乐音量和平衡每次播放都会被重新重置为默认值,你的设置失效了,而界面上没有做回馈,我修改了他的这一块的源码已解决这个问题。

想要支持全格式但还要兼顾电平脉冲(Javafx自带的脉冲效果很水),JavaMod不支持aac,mp4,m4a等格式

javafx自己本身就有音频相关解码器,mp4,m4a,mp3,aac等格式不在话下都是支持的,但是它提供的音乐可视化效果不尽人意,所以还是希望使用大佬们的三方插件来对aac,m4a和mp4做支持,实在是找不到对应的解码包我也懒得找了,JAVE是个不错的选择,拿来做格式转换然后再播放。JAVE功能很强大 ,几乎全格式支持,并且转换后音乐的标签也不会丢失。

 

再说说爬虫吧

我做的这个音乐播放器,爬取了酷狗,千千和虾米的搜索页面,酷狗(最简单)和千千简单,虾米相当费劲,虾米的音频url使用了恺撒加密,本人也是花了近一周的时间去解,到目前还是有低概率解析不出来,不过不是很影响因为加密结果是动态生成的,本次不成功可以再搜一次再解析。

写在最后

研究这些开源的软件,本着一个可以自定义界面的兴趣去做的,如果你有心思去做,那当然都是有办法去实现的。