C#deezer sdk包装器播放音乐

时间:2016-04-08 13:42:11

标签: c# api sdk deezer

我试图将Deezer的最后一个原生SDK库用到C#WPF应用程序中播放一首歌(与只允许抓取信息但不播放音乐的JSON库相对)。 不幸的是,我无法通过oAuth令牌进行身份验证,因此无法使其正常工作。我可以获取oAuth令牌,但SDK总是在Connect.Start()调用上返回USER_ACCESS_TOKEN_FAILED,即使我在通过SetAccessToken方法调用之前设置好令牌(通过其网站上的API资源管理器验证)。

有人可以帮我解决这个问题吗?

重现应用程序的步骤: - 从deezer developper网站下载最新的SDK。 - 创建WPF应用程序项目 - 在其属性Build部分中,取消选中Prefer 32 bits并选中Allow unsafe code - 将libdeezer.x64.dll添加到您的项目中,并在构建操作中将其设置为none,并复制操作以在较新时复制。 - 在主视图中,添加webbrowser并将Navigating事件添加到后面的代码中。 - 创建一个类并粘贴此代码(这是本机C ++ / CLI包装器):

 using System;
using System.Collections;
using System.Runtime.InteropServices;
// make this binding dependent on WPF, but easier to use
using System.Windows.Threading;

// http://www.codeproject.com/Articles/339290/PInvoke-pointer-safety-Replacing-IntPtr-with-unsaf

namespace Deezer
{
    // called with userdata Dispatcher on connect events
    public delegate void ConnectOnEventCb(Connect connect, ConnectEvent connectEvent, DispatcherObject userdata);
    public delegate void PlayerOnEventCb(Player player, PlayerEvent playerEvent, DispatcherObject userdata);

    // to be in sync with dz_connect_configuration
    [StructLayout(LayoutKind.Sequential)]
    public class ConnectConfig
    {
        public string ccAppId;
        public string ccAppSecret;

        public string ccUserProfilePath;

        public Dispatcher ccConnectUserdata;
        public ConnectOnEventCb ccConnectEventCb;
    }

    public class ConnectEvent
    {
        internal CONNECT_EVENT_TYPE eventType;

        /* two design strategies:
         * - we could keep a reference to CONNECT_EVENT* with dz_object_retain and call method on the fly
         * - we extract all info in constructor and have pure managed object
         * 
         * here we keep the second option, because we have to have a managed object anyway, and it's 
         * a lot fewer unsafe method to expose, even though it's making a lot of calls in the constructor..
         */
        public unsafe static ConnectEvent newFromLibcEvent(CONNECT_EVENT* libcConnectEventHndl)
        {
            CONNECT_EVENT_TYPE eventType;
            unsafe
            {
                eventType = dz_connect_event_get_type(libcConnectEventHndl);
            }
            switch (eventType)
            {
                case CONNECT_EVENT_TYPE.USER_ACCESS_TOKEN_OK:
                    string accessToken;
                    unsafe
                    {
                        IntPtr libcAccessTokenString = dz_connect_event_get_access_token(libcConnectEventHndl);
                        accessToken = Marshal.PtrToStringAnsi(libcAccessTokenString);
                    }
                    return new NewAccessTokenConnectEvent(accessToken);
                default:
                    return new ConnectEvent(eventType);
            }
        }

        public ConnectEvent(CONNECT_EVENT_TYPE eventType)
        {
            this.eventType = eventType;
        }

        public CONNECT_EVENT_TYPE GetEventType()
        {
            return eventType;
        }

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe CONNECT_EVENT_TYPE dz_connect_event_get_type(
            CONNECT_EVENT* dzConnectEvent);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe IntPtr dz_connect_event_get_access_token(
            CONNECT_EVENT* dzConnectEvent);
    }

    public class NewAccessTokenConnectEvent : ConnectEvent
    {
        string accessToken;

        public NewAccessTokenConnectEvent(string accessToken)
            : base(CONNECT_EVENT_TYPE.USER_ACCESS_TOKEN_OK)
        {
            this.accessToken = accessToken;
        }

        public string GetAccessToken()
        {
            return accessToken;
        }
    }

    unsafe public class Connect
    {
        // hash
        static Hashtable refKeeper = new Hashtable();

        internal unsafe CONNECT* libcConnectHndl;
        internal ConnectConfig connectConfig;

        public unsafe Connect(ConnectConfig cc)
        {
            NativeMethods.LoadClass();
            //ConsoleHelper.AllocConsole();
            // attach a console to parent process (launch from cmd.exe)
            //ConsoleHelper.AttachConsole(-1);

            CONNECT_CONFIG libcCc = new CONNECT_CONFIG();

            connectConfig = cc;

            IntPtr intptr = new IntPtr(this.GetHashCode());

            refKeeper[intptr] = this;

            libcCc.ccAppId = cc.ccAppId;
            libcCc.ccAppSecret = cc.ccAppSecret;

            libcCc.ccUserProfilePath = UTF8Marshaler.GetInstance(null).MarshalManagedToNative(cc.ccUserProfilePath);
            libcCc.ccConnectEventCb = delegate (CONNECT* libcConnect, CONNECT_EVENT* libcConnectEvent, IntPtr userdata)
            {
                Connect connect = (Connect)refKeeper[userdata];
                ConnectEvent connectEvent = ConnectEvent.newFromLibcEvent(libcConnectEvent);
                Dispatcher dispather = connect.connectConfig.ccConnectUserdata;

                dispather.Invoke(connect.connectConfig.ccConnectEventCb, connect, connectEvent, connect.connectConfig.ccConnectUserdata);
            };

            libcConnectHndl = dz_connect_new(libcCc);

            UTF8Marshaler.GetInstance(null).CleanUpNativeData(libcCc.ccUserProfilePath);
        }

        public int Start()
        {
            int ret;
            ret = dz_connect_activate(libcConnectHndl, new IntPtr(this.GetHashCode()));
            return ret;
        }

        public string DeviceId()
        {
            IntPtr libcDeviceId = dz_connect_get_device_id(libcConnectHndl);

            if (libcDeviceId == null)
            {
                return null;
            }

            return Marshal.PtrToStringAnsi(libcDeviceId);
        }

        public int SetAccessToken(string accessToken)
        {
            int ret;
            ret = dz_connect_set_access_token(libcConnectHndl, IntPtr.Zero, IntPtr.Zero, accessToken);
            return ret;
        }

        public int SetSmartCache(string path, int quotaKb)
        {
            int ret;
            ret = dz_connect_cache_path_set(libcConnectHndl, IntPtr.Zero, IntPtr.Zero, path);
            ret = dz_connect_smartcache_quota_set(libcConnectHndl, IntPtr.Zero, IntPtr.Zero, quotaKb);
            return ret;
        }

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe CONNECT* dz_connect_new(
            [In, MarshalAs(UnmanagedType.LPStruct)]
            CONNECT_CONFIG lpcc);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe IntPtr dz_connect_get_device_id(
            CONNECT* dzConnect);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_connect_activate(
            CONNECT* dzConnect, IntPtr userdata);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_connect_set_access_token(
            CONNECT* dzConnect, IntPtr cb, IntPtr userdata, string access_token);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_connect_cache_path_set(
            CONNECT* dzConnect, IntPtr cb, IntPtr userdata,
            [MarshalAs(UnmanagedType.CustomMarshaler,
              MarshalTypeRef=typeof(UTF8Marshaler))]
              string local_path);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_connect_smartcache_quota_set(
            CONNECT* dzConnect, IntPtr cb, IntPtr userdata,
              int quota_kB);
    }

    public class PlayerEvent
    {
        internal PLAYER_EVENT_TYPE eventType;

        /* two design strategies:
         * - we could keep a reference to PLAYER_EVENT* with dz_object_retain and call method on the fly
         * - we extract all info in constructor and have pure managed object
         * 
         * here we keep the second option, because we have to have a managed object anyway, and it's 
         * a lot fewer unsafe method to expose, even though it's making a lot of calls in the constructor..
         */
        public unsafe static PlayerEvent newFromLibcEvent(PLAYER_EVENT* libcPlayerEventHndl)
        {
            PLAYER_EVENT_TYPE eventType;
            unsafe
            {
                eventType = dz_player_event_get_type(libcPlayerEventHndl);
            }
            switch (eventType)
            {
                default:
                    return new PlayerEvent(eventType);
            }
        }

        public PlayerEvent(PLAYER_EVENT_TYPE eventType)
        {
            this.eventType = eventType;
        }

        public PLAYER_EVENT_TYPE GetEventType()
        {
            return eventType;
        }

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe PLAYER_EVENT_TYPE dz_player_event_get_type(
            PLAYER_EVENT* dzPlayerEvent);
    }

    unsafe public class Player
    {
        // hash
        static Hashtable refKeeper = new Hashtable();

        internal unsafe PLAYER* libcPlayerHndl;
        internal Connect connect;
        internal libcPlayerOnEventCb eventcb;


        public unsafe Player(Connect connect, object observer)
        {
            IntPtr intptr = new IntPtr(this.GetHashCode());

            refKeeper[intptr] = this;

            libcPlayerHndl = dz_player_new(connect.libcConnectHndl, intptr);

            this.connect = connect;
        }

        public int Start(PlayerOnEventCb eventcb)
        {
            int ret;
            ret = dz_player_activate(libcPlayerHndl, new IntPtr(this.GetHashCode()));

            this.eventcb = delegate (PLAYER* libcPlayer, PLAYER_EVENT* libcPlayerEvent, IntPtr userdata)
            {
                Player player = (Player)refKeeper[userdata];
                PlayerEvent playerEvent = PlayerEvent.newFromLibcEvent(libcPlayerEvent);
                Dispatcher dispather = player.connect.connectConfig.ccConnectUserdata;

                dispather.Invoke(eventcb, player, playerEvent, connect.connectConfig.ccConnectUserdata);
            };

            ret = dz_player_set_event_cb(libcPlayerHndl, this.eventcb);
            return ret;
        }

        public int LoadStream(string url)
        {
            int ret;
            ret = dz_player_load(libcPlayerHndl, IntPtr.Zero, IntPtr.Zero, url);
            return ret;
        }

        public int Play(int idx)
        {
            int ret;
            ret = dz_player_play(libcPlayerHndl, IntPtr.Zero, IntPtr.Zero, idx, TRACKLIST_AUTOPLAY_MODE.ONE);
            return ret;
        }

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe PLAYER* dz_player_new(CONNECT* lpcc, IntPtr userdata);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_player_set_event_cb(PLAYER* lpcc, libcPlayerOnEventCb cb);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_player_activate(
            PLAYER* dzPlayer, IntPtr userdata);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_player_load(
            PLAYER* dzPlayer, IntPtr cb, IntPtr userdata, string url);

        [DllImport("libdeezer.x64.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern unsafe int dz_player_play(
            PLAYER* dzPlayer, IntPtr cb, IntPtr userdata, int idx, TRACKLIST_AUTOPLAY_MODE mode);
    }

    unsafe public struct CONNECT_EVENT { };
    public enum CONNECT_EVENT_TYPE
    {
        USER_OFFLINE_AVAILABLE,            /**< */

        USER_ACCESS_TOKEN_OK,              /**< */
        USER_ACCESS_TOKEN_FAILED,          /**< */

        USER_LOGIN_OK,                     /**< */
        USER_LOGIN_FAIL_NETWORK_ERROR,     /**< */
        USER_LOGIN_FAIL_BAD_CREDENTIALS,   /**< */
        USER_LOGIN_FAIL_USER_INFO,         /**< */

        USER_SYNC_LICENSES_OK,             /**< */
        USER_SYNC_LICENSES_FAILED,         /**< */

        LAST,                   /**< first invalid value for dz_connect_event_t  */
    };

    public enum TRACKLIST_AUTOPLAY_MODE
    {
        ONE,
        ONE_REPEAT,

        NEXT,
        NEXT_REPEAT,

        RANDOM,
        RANDOM_REPEAT,

        LAST,
    };

    unsafe public struct UTF8STRING { };

    unsafe public struct CONNECT { };

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    unsafe public delegate void libcConnectOnEventCb(CONNECT* libcConnect, CONNECT_EVENT* libcConnectEvent, IntPtr userdata);

    [StructLayout(LayoutKind.Sequential)]
    public class CONNECT_CONFIG
    {
        public string ccAppId;
        public string ccAppSecret;

        public string ccProductId;
        public string ccProductBuildId;

        //[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(UTF8Marshaler))]
        public IntPtr ccUserProfilePath;

        public libcConnectOnEventCb ccConnectEventCb;
    }


    unsafe public struct PLAYER_EVENT { };
    public enum PLAYER_EVENT_TYPE
    {
        /** data access related event */
        LIMITATION_FORCED_PAUSE,             /**< another deezer player session was created elsewhere, the player entered pause mode */

        /** track selection related event */
        PLAYLIST_TRACK_NOT_AVAILABLE_OFFLINE,/**< you're offline, and the track is not available */
        PLAYLIST_TRACK_NO_RIGHT,             /**< you don't have the right to render this track */
        PLAYLIST_SKIP_NO_RIGHT,              /**< you're on a radio, and you had no right to do skip */
        PLAYLIST_TRACK_SELECTED,             /**< a track is selected and is going to be read (radio) */

        /** data loading related event */
        MEDIASTREAM_DATA_READY,              /**< data is ready to be injected into audio output (first data after a play) */
        MEDIASTREAM_DATA_READY_AFTER_SEEK,   /**< data is ready to be injected into audio output (first data after a seek) */

        /** play (audio rendering on output) related event */
        RENDER_TRACK_START_FAILURE,       /**< error on trying to start a track */
        RENDER_TRACK_START,       /**< a track start playing */
        RENDER_TRACK_END,         /**< a track finished playing because of end of stream */
        RENDER_TRACK_PAUSED,      /**< paused is effective */
        RENDER_TRACK_SEEKING,     /**< waiting for new data on seek */
        RENDER_TRACK_UNDERFLOW,   /**< underflow happened when playing a track */
        RENDER_TRACK_RESUMED,     /**< player resumed play after a underflow or a pause */
        RENDER_TRACK_REMOVED,     /**< player stopped playing a track */

        LAST,                   /**< first invalid value for dz_player_event_t  */
    };

    unsafe public struct PLAYER { };

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    unsafe public delegate void libcPlayerOnEventCb(PLAYER* libcPlayer, PLAYER_EVENT* libcPlayerEvent, IntPtr userdata);

    // trick from http://stackoverflow.com/questions/1573724/cpu-architecture-independent-p-invoke-can-the-dllname-or-path-be-dynamic
    // but actually SetDllDirectory works better (for pthread.dll)
    public static class NativeMethods
    {
        // call this to load this class
        public static void LoadClass()
        {
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool SetDllDirectory(string lpPathName);

        [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern IntPtr LoadLibrary(string lpFileName);

        static NativeMethods()
        {
            string arch;
            string basePath = System.IO.Path.GetDirectoryName(typeof(NativeMethods).Assembly.Location);


            if (IntPtr.Size == 4)
                arch = "i386";
            else
                arch = "x86_64";

            System.Diagnostics.Debug.WriteLine("using arch: " + arch);

            SetDllDirectory(System.IO.Path.Combine(basePath, arch));
#if false // can be used to debug library loading
            IntPtr hExe = LoadLibrary("libdeezer.x64.dll");

            if (hExe == IntPtr.Zero)
            {
                Win32Exception ex = new Win32Exception(Marshal.GetLastWin32Error());
                System.Console.WriteLine("exception:" + ex);
                throw ex;
            }
#endif
        }

    }

    // http://stackoverflow.com/questions/10415807/output-console-writeline-from-wpf-windows-applications-to-actual-console
    public class ConsoleHelper
    {
        /// <summary>
        /// Allocates a new console for current process.
        /// </summary>
        [DllImport("kernel32.dll")]
        public static extern Boolean AllocConsole();

        [DllImport("Kernel32.dll")]
        public static extern bool AttachConsole(int processId);

        /// <summary>
        /// Frees the console.
        /// </summary>
        [DllImport("kernel32.dll")]
        public static extern Boolean FreeConsole();
    }

    // http://www.codeproject.com/Articles/138614/Advanced-Topics-in-PInvoke-String-Marshaling
    public class UTF8Marshaler : ICustomMarshaler
    {
        static UTF8Marshaler static_instance;

        // maybe we could play with WideCharToMultiByte too and avoid Marshal.Copy
        // http://stackoverflow.com/questions/537573/how-to-get-intptr-from-byte-in-c-sharp
        /*
            Byte[] byNewData = null;

            iNewDataLen = NativeMethods.WideCharToMultiByte(NativeMethods.CP_UTF8, 0, cc.ccUserProfilePath, -1, null, 0, IntPtr.Zero, IntPtr.Zero);
            Console.WriteLine("iNewDataLen:" + iNewDataLen + " len:" + cc.ccUserProfilePath.Length + " ulen:" + iNewDataLen);
            byNewData = new Byte[iNewDataLen];
            iNewDataLen = NativeMethods.WideCharToMultiByte(NativeMethods.CP_UTF8, 0, cc.ccUserProfilePath, cc.ccUserProfilePath.Length, byNewData, iNewDataLen, IntPtr.Zero, IntPtr.Zero);

            libcCc.ccUserProfilePath = Marshal.UnsafeAddrOfPinnedArrayElement(byNewData, 0);
         */
        public IntPtr MarshalManagedToNative(object managedObj)
        {
            if (managedObj == null)
                return IntPtr.Zero;
            if (!(managedObj is string))
                throw new MarshalDirectiveException(
                       "UTF8Marshaler must be used on a string.");

            // not null terminated
            byte[] strbuf = System.Text.Encoding.UTF8.GetBytes((string)managedObj);
            IntPtr buffer = Marshal.AllocHGlobal(strbuf.Length + 1);
            Marshal.Copy(strbuf, 0, buffer, strbuf.Length);

            // write the terminating null
            Marshal.WriteByte(buffer + strbuf.Length, 0);
            return buffer;
        }
        public unsafe object MarshalNativeToManaged(IntPtr pNativeData)
        {
            byte* walk = (byte*)pNativeData;

            // find the end of the string
            while (*walk != 0)
            {
                walk++;
            }
            int length = (int)(walk - (byte*)pNativeData);

            // should not be null terminated
            byte[] strbuf = new byte[length];
            // skip the trailing null
            Marshal.Copy((IntPtr)pNativeData, strbuf, 0, length);
            string data = System.Text.Encoding.UTF8.GetString(strbuf);
            return data;
        }

        public void CleanUpNativeData(IntPtr pNativeData)
        {
            Marshal.FreeHGlobal(pNativeData);
        }

        public void CleanUpManagedData(object managedObj)
        {
        }

        public int GetNativeDataSize()
        {
            return -1;
        }

        public static ICustomMarshaler GetInstance(string cookie)
        {
            if (static_instance == null)
            {
                return static_instance = new UTF8Marshaler();
            }
            return static_instance;
        }

        [DllImport("kernel32.dll")]
        public static extern int WideCharToMultiByte(uint CodePage, uint dwFlags,
           [MarshalAs(UnmanagedType.LPWStr)] string lpWideCharStr, int cchWideChar,
           [MarshalAs(UnmanagedType.LPArray)] Byte[] lpMultiByteStr, int cbMultiByte, IntPtr lpDefaultChar,
           IntPtr lpUsedDefaultChar);

        public const uint CP_UTF8 = 65001;
    }
}

然后,将此代码添加到

后面的主视图代码中
using System;
using System.Windows;
using Deezer;
using System.Windows.Threading;
using System.Web;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            dWebBrowser.Navigate("https://connect.deezer.com/fr/oauth/auth.php?app_id=your_app_id_here&redirect_uri=http://your_domain_here&perms=basic_access&response_type=token");
        }

        private void dWebBrowser_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e)
        {
            if (e.Uri.ToString().Contains("replace with your domain") && e.Uri.ToString().Contains("access_token"))
            {
                e.Cancel = true;
                String token = HttpUtility.ParseQueryString(e.Uri.ToString().Replace("#", "&")).Get("access_token");
                PlaySong(token);
            }
        }

        private void PlaySong(String token)
        {
            try
            {
                ConnectConfig dConfig = new ConnectConfig();
                dConfig.ccAppId = "replace with your APP ID";
                dConfig.ccAppSecret = "replace with your SECRET APP CODE";
               // dConfig.ccConnectEventCb = dConnectOnEventCb; (optional)
                dConfig.ccConnectUserdata = this.Dispatcher;  
                dConfig.ccUserProfilePath = @"D:\dztemp";

                Connect dConnect = new Connect(dConfig);
                dConnect.SetAccessToken(token);
                dConnect.SetSmartCache(@"D:\dztemp", 2000000);
                CONNECT_EVENT_TYPE resp = (CONNECT_EVENT_TYPE)dConnect.Start();
                String devId = dConnect.DeviceId();

                Object dObserver = null;
                Player dPlayer = new Player(dConnect, dObserver);
                PLAYER_EVENT_TYPE respPlayerStart = (PLAYER_EVENT_TYPE)dPlayer.Start(dPlayerOnEventCb);
                PLAYER_EVENT_TYPE respPlayerLoad = (PLAYER_EVENT_TYPE)dPlayer.LoadStream("dzmedia:///track/3135556");
                PLAYER_EVENT_TYPE respPlayerPlay = (PLAYER_EVENT_TYPE)dPlayer.Play(0);
            }
            catch (Exception ex)
            {
                throw ex;
            }

        }


        public void dConnectOnEventCb(Connect connect, ConnectEvent connectEvent, DispatcherObject userdata)
        {
            this.Dispatcher.Invoke(connect.connectConfig.ccConnectEventCb, connect, connectEvent, connect.connectConfig.ccConnectUserdata);
        }

        public void dPlayerOnEventCb(Player player, PlayerEvent playerEvent, DispatcherObject userdata)
        {
            String a = "";
        }


    }
}

然后启动您的应用。 然后由(CONNECT_EVENT_TYPE)返回的CONNECT_EVENT_TYPE dConnect.Start()应该是USER_ACCESS_TOKEN_OK,但我总是得到USER_ACCESS_TOKEN_FAILED。 尝试拦截生成的令牌,并通过deezer API浏览器检查它是否是有效令牌。应该没问题。所以我真的不明白这里发生了什么......

1 个答案:

答案 0 :(得分:0)

为了确定,你有没有替换:

dConfig.ccAppId = "replace with your APP ID";
dConfig.ccAppSecret = "replace with your SECRET APP CODE";

使用 应用程序ID 应用程序密钥

dWebBrowser.Navigate("https://connect.deezer.com/fr/oauth/auth.php?app_id=your_app_id_here&redirect_uri=http://your_domain_here&perms=basic_access&response_type=token");

使用 app_id redirect_uri

修改

由于您拥有C#包装器的版本,因此enum稍有改动。 请检查您在包装中使用的所有枚举。例如:

dz_connect_event_t似乎与C#CONNECT_EVENT_TYPE

不一致

请同时查看PLAYER_EVENT_TYPE枚举,CONNECT_CONFIG结构,...

CONNECT_CONFIG应该更像:

public class CONNECT_CONFIG
{
    public string ccAppId;

    public string ccProductId;
    public string ccProductBuildId;

    public IntPtr ccUserProfilePath;

    public libcConnectOnEventCb ccConnectEventCb;

    public string ccAnonymousBlob;

    public libcAppCrashDelegate ccAppCrashDelegate;
}