0
0

I am having problems with getting my application to handle callbacks the way I want them to. I am using MS VC++ 6.0 and FMOD version 3.5. The application will only play mp3 files. The problem I am having is this:

When a song (song1) is playing and you call next() it does go to the next song (song2) as it’s supposed to. Then the callback from song1 comes and triggers another next() and now you’re at song3. I’ve figured out how to stop this behavior so that it only will go to song2 like it’s supposed to, but this solution breaks the other part of what I want it to do.
When a song is over and it calls the callback function, it is supposed to go to the next song.

My challenge is in trying to write a more intelligent callback method so that it can tell if it needs to go to the next song or not. The only condition that should cause the callback function to go to the next song is that the current song is over and it’s time to go to the next one. Posted below is the code I have (which doesn’t work correctly for the callbacks).

Any assistance would be very appreciated as I’ve been pulling my hair out over this for 2 or 3 days… BTW, the API in the code below is not set in stone by any means, methods can be added/removed and parameters changed without difficulties.

code from application:

playlist.h
[quote:16cwtvui]

ifndef __PLAYLIST_H

define __PLAYLIST_H

define SORT_RANDOM 0

define SORT_ASC_TITLE 1

define SORT_ASC_ARTIST 2

define SORT_ASC_FILE 3

define SORT_DSC_TITLE 4

define SORT_DSC_ARTIST 5

define SORT_DSC_FILE 6

include “..\fmod\fmod.h”

include “songinfo.h”

include <windows.h>

include <search.h>

include <string>

include <vector>

using namespace std;

class PlayListController;

class PlayList
{
public:
PlayList(int idNum, string plistName, PlayListController *master);
~PlayList();

// return the id number of the playlist
int GetID();

// return the name of the playlist
string GetName();

// add a song to the playlist
// if toDB is true, add it to the database as well
void AddSong(songInfo *song);

// remove a song from the playlist
// if toDB is true, remove it from the database as well
void RemoveSong(songInfo *song);
void RemoveSong(int songID);

// remove the song at the specified index
// if toDB is true, remove it from the database as well
void RemoveSongAt(int index);

// returns true if the specified song is in this playlist
bool ContainsSong(songInfo *song);
bool ContainsSong(int songID);

// returns the index of the song in the playlist
// returns -1 if the song is not in the playlist
int SongIndex(songInfo *song);
int SongIndex(int songID);

// returns the number of songs in the playlist
int GetCount();

// returns the song at the specified index
// returns NULL if the index is not in [0..n-1] where
//   n is the number of songs in the playlist
songInfo *GetSongAt(int index);

// returns the current song
// returns NULL if there is nothing in the playlist
songInfo *GetCurrentSong();

// returns the index of the current song
int GetCurrentSongIndex();

// returns how the list is sorted
int GetHowSorted();

// sort the list in the specified manner
void Sort(int how);

// play the song at the specified index
void PlaySongAt(int index);

// play the current song
bool Play();

// pause the current song
void Pause();

// stop the current song
void Stop();

// go to the next song
// loop to the first song if we played the last song
void Next();

// go to the previous song
// loop to the last song if we played the first song
void Prev();

// jump forward the specified percentage
// if give a negative percentage, jumps backwards that percent
void JumpPercent(int percent);

// returns true if the song is paused
bool IsPaused();

// returns true if the song is playing
bool IsPlaying();

// returns the bitrate of the current song
int GetBitRate();

// returns the length of the current song in seconds
int GetLengthInSeconds();

// returns the percentage of CPU time this is using
// range is 0.0 - 100.0
float GetCPUUsage();

protected:
bool Play(songInfo *song);

int id;
string name;
vector&lt;songInfo*&gt; songs;
int howSorted;
int currentIndex;
int currentChannel;


bool isPaused;
FSOUND_STREAM* currentSound;

};

endif __PLAYLIST_H

[/quote:16cwtvui]

playlist.cpp
[quote:16cwtvui]

include “playlist.h”

include “playlistcontroller.h”

pragma warning( disable : 4786 )

typedef struct
{
int playlistID;
int songID;
} sCallBack;

PlayListController *controller;
signed char SoundCallback(FSOUND_STREAM *stream, void *buf, int len, int param);

PlayList::PlayList(int idNum, string plistName, PlayListController *master)
{
id = idNum;
name = plistName;
howSorted = SORT_RANDOM;
currentIndex = -1;
isPaused = false;
controller = master;
currentSound = NULL;

currentChannel = -1;

}

PlayList::~PlayList()
{
}

int PlayList::GetID()
{
return id;
}

string PlayList::GetName()
{
return name;
}

void PlayList::AddSong(songInfo* song)
{
if(!song)
return;

if( ContainsSong(song) )
    return;

songs.push_back(song);

if(currentIndex == -1)
    currentIndex = 0;

}

void PlayList::RemoveSong(songInfo* song)
{
RemoveSongAt(SongIndex(song));
}

void PlayList::RemoveSong(int songID)
{
RemoveSongAt(SongIndex(songID));
}

void PlayList::RemoveSongAt(int index)
{
if(index < 0)
return;
if(index >= songs.size())
return;

vector&lt;songInfo*&gt;::iterator itr;
itr = songs.begin();
itr += index;
songs.erase(itr);

}

bool PlayList::ContainsSong(songInfo* song)
{
if(!song)
return false;

return ContainsSong(song-&gt;id);

}

bool PlayList::ContainsSong(int songID)
{
return (SongIndex(songID) != -1);
}

int PlayList::SongIndex(songInfo* song)
{
if(!song)
return -1;

return SongIndex(song-&gt;id);

}

int PlayList::SongIndex(int songID)
{
int i, size;

size = songs.size();
for(i = 0; i &lt; size; i++)
    if(songs[i]-&gt;id == songID)
        return i;

return -1;

}

int PlayList::GetCount()
{
return songs.size();
}

songInfo* PlayList::GetSongAt(int index)
{
if(index < 0)
return NULL;

if(index &gt;= songs.size())
    return NULL;

return songs[index];

}

songInfo* PlayList::GetCurrentSong()
{
int size = songs.size();

if(size == 0)
    return NULL;

if(currentIndex == size)
    currentIndex = size - 1;

if(currentIndex &lt; 0)
    currentIndex = 0;

return songs[currentIndex];

}

int PlayList::GetCurrentSongIndex()
{
return currentIndex;
}

int PlayList::GetHowSorted()
{
return howSorted;
}

void PlayList::Sort(int how)
{
if(how < SORT_RANDOM) return;
if(how > SORT_DSC_FILE) return;

howSorted = how;
int i, num, size;
songInfo **tmp;

num = songs.size();
if(num &lt;= 1)
    return;

int currentSongID = -1;
if(GetCurrentSong())
    currentSongID = GetCurrentSong()-&gt;id;

if(how == SORT_RANDOM)
{
    // randomize the list since it's sorted already
    vector&lt;songInfo *&gt; tmp_list;
    vector&lt;songInfo *&gt;::iterator ptr;
    unsigned int seed = GetTickCount();
    srand(seed);
    int num;

    while(songs.size() &gt; 0)
    {
        // go to beginning of playlist
        ptr = songs.begin();

        // get a random number in the range [0..playlist.size() -1]
        num = (rand() * seed) % songs.size();

        // put this element into the temp list
        tmp_list.push_back( songs[num] );

        // remove this element from the playlist
        ptr += num;
        songs.erase(ptr);
    }

    // set playlist equal to the temp list
    songs = tmp_list;
}
else
{
    tmp = new songInfo*[num];
    for(i = 0; i &lt; num; i++)
        tmp[i] = songs[i];
    songs.clear();

    size = sizeof(struct songInfo);

         if(how == SORT_ASC_TITLE)  qsort(tmp, num, size, SortByTitleAsc);
    else if(how == SORT_ASC_ARTIST) qsort(tmp, num, size, SortByArtistAsc);
    else if(how == SORT_ASC_FILE)   qsort(tmp, num, size, SortByFileAsc);
    else if(how == SORT_DSC_TITLE)  qsort(tmp, num, size, SortByTitleDsc);
    else if(how == SORT_DSC_ARTIST) qsort(tmp, num, size, SortByArtistDsc);
    else if(how == SORT_DSC_FILE)   qsort(tmp, num, size, SortByFileDsc);
    else {}

    for(i = 0; i &lt; num; i++)
        songs.push_back(tmp[i]);
    delete [] tmp;
}

if(currentSongID != -1)
    currentIndex = SongIndex(currentSongID);

}

void PlayList::PlaySongAt(int index)
{
if(index < 0)
return;
if(index >= songs.size())
return;

Stop();

currentIndex = index;

Play();

}

bool PlayList::Play()
{
if(currentSound)
Stop();

songInfo *tmp = GetCurrentSong();
if(!tmp)
    return false;

if( Play(tmp) )
    return true;
else
{
    RemoveSong(tmp);
    isPaused = false;
    return Play();
}

}

void PlayList::Pause()
{
isPaused = !isPaused;
FSOUND_SetPaused(FSOUND_ALL, isPaused);
}

void PlayList::Stop()
{
if(currentSound)
{
FSOUND_Stream_Stop(currentSound);
FSOUND_Stream_Close(currentSound);
}
currentSound = NULL;
isPaused = false;
}

void PlayList::Next()
{
Stop();

currentIndex++;
if(currentIndex &gt;= songs.size())
    currentIndex = 0;

Play();

}

void PlayList::Prev()
{
Stop();

currentIndex--;
if(currentIndex &lt; 0)
    currentIndex = songs.size() - 1;

Play();

}

void PlayList::JumpPercent(int percent)
{
if(percent == 0)
return;

// check for negative percent
bool negative = percent &lt; 0;
if(negative)
    percent = abs(percent);

// make sure it's in the range 1..99
percent %= 100;

// we want to get x% of the total songs in this list
int num = (songs.size() * 100) / percent;
int newIndex = currentIndex;

if(negative)
{
    newIndex -= num;
    if(newIndex &lt; 0)
        newIndex = songs.size() - newIndex - 1;
}
else
    newIndex += num;

newIndex %= songs.size();

// if we end up at the same index we started at, go
// to the next or previous song
if(newIndex == currentIndex)
{
    if(negative)
        Prev();
    else
        Next();
}
else
{
    Stop();

    currentIndex = newIndex;

    Play();
}

}

bool PlayList::IsPaused()
{
return isPaused;
}

bool PlayList::IsPlaying()
{
BOOL b = FSOUND_IsPlaying(currentChannel);
return (b == TRUE);
}

int PlayList::GetBitRate()
{
if(!currentSound)
return 0;

// <!-- m --><a class="postlink" href="http://52.88.2.202/forum/viewtopic.php?t=504">http://52.88.2.202/forum/viewtopic.php?t=504</a><!-- m -->
unsigned int lenms, lenbytes, kbps; 
lenms = FSOUND_Stream_GetLengthMs(currentSound); 
lenbytes = FSOUND_Stream_GetLength(currentSound); 
kbps = lenbytes / (lenms / 1000) /1000 * 8; 

return kbps;

}

int PlayList::GetLengthInSeconds()
{
if(!currentSound)
return 0;

return (FSOUND_Stream_GetLengthMs(currentSound) / 1000);

}

float PlayList::GetCPUUsage()
{
return FSOUND_GetCPUUsage();
}

bool PlayList::Play(songInfo *song)
{
if(!song)
return false;

currentSound = FSOUND_Stream_OpenFile(song-&gt;filename.c_str(), FSOUND_LOOP_OFF | FSOUND_HW3D | FSOUND_STREAMABLE, 0);
if(!currentSound)
    return false;

currentChannel = FSOUND_Stream_Play(FSOUND_FREE, currentSound);
FSOUND_SetVolume(currentChannel, 196);

sCallBack *param = new sCallBack;
param-&gt;playlistID = id;
param-&gt;songID = song-&gt;id;

FSOUND_Stream_SetEndCallback(currentSound, SoundCallback, (int)param);

controller-&gt;NotifyIsPlaying(this);

return true;

}

signed char SoundCallback(FSOUND_STREAM stream, void *buf, int len, int param)
{
sCallBack *call = (sCallBack
)param;
if(!call)
return ‘1’;

int songID = call-&gt;songID;
int playID = call-&gt;playlistID;
delete call;

PlayList *tmp1 = controller-&gt;GetPlayList(playID);
if(!tmp1)
    return '2';

PlayList *tmp2 = controller-&gt;GetCurrentPlayList();
if(!tmp2)
{
    tmp1-&gt;Stop();
    return '3';
}

// code goes here for fixing callbacks so it only will call
// tmp1->Next() if it is a result of the song being over,
// not a result of a call of Next()

return '0';

}
[/quote:16cwtvui]

  • You must to post comments
0
0

It is 100% reproducable. Processor usage spikes to 100% after a callback. If I do a FSOUND_Close in the callback, it causes an illegal operation. I can zip and mail the project files to you if you want to investigate further.

Thanks,
James

  • You must to post comments
0
0

Gack! ๐Ÿ˜ฎ That’s a lot of code. And I see a lot of “style issues”. But I don’t want to criticize… too much. ๐Ÿ˜€

Why don’t you try disabling the callback before you issue the Stop() in Next()? And after you start playing, reenable the callback. That sounds like a good, solid solution, since the callback is only needed to do the Next() upon the actual end of the stream. So, whenever you force a Stop(), always disable the callback first. That make sense?

P.S. Your code isn’t that bad. I just get a little picky on certain coding practices. But this isn’t the forum for proper coding procedures.

P.P.S. The disabling/enabling could be done in the Stop() and Play() methods so you never have to worry about it. That’s even a cleaner solution. Less repeated code.

  • You must to post comments
0
0

Brett,

Thanks for sending me version 3.6. It fixed things perfectly!

James

  • You must to post comments
0
0

Serac,

Thanks for your reply! I’ld like to apoligize to how this renders code that is cut and pasted (it seems to have demolished the tabs). Other than the tabs, what change would you want for coding style? Would you prefer the braces as in the following snippet as opposed to how my code is written?

[quote:xyt79jdd]
PlayList::PlayList() {
// constructor code here
for(int i = 0; i < 5; i++) {
// do something here
}
}
[/quote:xyt79jdd]

I’m just feeling kinda lost because I understand what you are saying about when callback should be disabled, but I don’t see how to implement it. Changing the code as follows causes an illegal operation at the end of a song. When Next() is called and it’s not a callback, subsequent callbacks cause it to call Next() so it is “jumping” ahead in the playlist instead of just going to the next song. Here are the snippets of modified code:

[quote:xyt79jdd]
PlayList::PlayList(int idNum, string plistName, PlayListController *master)
{
id = idNum;
name = plistName;
howSorted = SORT_RANDOM;
currentIndex = -1;
isPaused = false;
controller = master;
currentSound = NULL;

currentChannel = -1;

// new code begins here
disableCallback = false;
// new code ends here

}

void PlayList::Next()
{
// new code begins here
if(IsPlaying())
disableCallback = true;
else
disableCallback = false;
// new code ends here

Stop();

currentIndex++;
if(currentIndex &gt;= songs.size())
    currentIndex = 0;

Play();

}

bool PlayList::Play(songInfo *song)
{
if(!song)
return false;

currentSound = FSOUND_Stream_OpenFile(song-&gt;filename.c_str(), FSOUND_LOOP_OFF | FSOUND_HW3D | FSOUND_STREAMABLE, 0);
if(!currentSound)
    return false;

currentChannel = FSOUND_Stream_Play(FSOUND_FREE, currentSound);
FSOUND_SetVolume(currentChannel, 196);

// new code begins here
if(!disableCallback)
{
// new code ends here

    sCallBack *param = new sCallBack;
    param-&gt;playlistID = id;
    param-&gt;songID = song-&gt;id;

    FSOUND_Stream_SetEndCallback(currentSound, SoundCallback, (int)param);

// new code begins here
}
// new code ends here

controller-&gt;NotifyIsPlaying(this);

return true;

}

signed char SoundCallback(FSOUND_STREAM stream, void *buf, int len, int param)
{
sCallBack *call = (sCallBack
)param;
if(!call)
return ‘1’;

int songID = call-&gt;songID;
int playID = call-&gt;playlistID;
delete call;

PlayList *tmp1 = controller-&gt;GetPlayList(playID);
if(!tmp1)
    return '2';

PlayList *tmp2 = controller-&gt;GetCurrentPlayList();
if(!tmp2)
{
    tmp1-&gt;Stop();
    return '3';
}

// code goes here for fixing callbacks so it only will call
// tmp1->Next() if it is a result of the song being over,
// not a result of a call of Next()
if(tmp1 == tmp2)
tmp1->Next();
else
tmp1->Stop();

return '0';

}
[/quote:xyt79jdd]

Can you provide some snippets that might avoid this? I’ve been up for too long and it’s probably something simple, just I’m not seeing it…

Thanks for the help!
James

  • You must to post comments
0
0

I had exactly this problem whilst implementing a player, and my solution was to set up a variable (in my case this was in the class wrapping FMod, but you could use a global). When you skip a track, set the variable. When the callback occurs, check the variable, and if it is not set,go on to the next song. If it is set, clear it and return.

e.g.

BOOL ForcedEnd = FALSE ;

Callback( … )
{
if ( ForcedEnd )
{
ForcedEnd = FALSE ;
return ;
}
else
{
// Code to start next track
}
}
SkipToNext ( )
{
// Code to do the skip

ForcedEnd = TRUE ;
}

  • You must to post comments
0
0

Phil,

Thanks for replying! I implemented the solution you recommended but found it doesn’t quite work how it’s supposed to.

When you start the application and have a song playing, when you call Next() directly (not from a callback) it jumps forward 2 songs instead of just going to the next one. When the callback from that song happens, nothing else happens. If you call Play(), you’re still at the same song that just got done playing.

If you just start the application and have a song playing, when the callback happens it get’s an illegal operation and shuts down the entire application.

Most of my code is in the first post in this thread. ForcedEnd is initialized to false at the start of the file. Here’s the modified Next() and Callback() methods.

[quote:ffiq27rb]
void PlayList::Next()
{
Stop();

currentIndex++;
if(currentIndex &gt; songs.size() - 1)
    currentIndex = 0;

Play();

ForcedEnd = true;

}

signed char SoundCallback(FSOUND_STREAM *stream, void *buf, int len, int param)
{
if(ForcedEnd)
{
ForcedEnd = false;
return ‘1’;
}
else
{
PlayList *tmp1 = (PlayList *)param;
if(!tmp1)
return ‘2’;

    PlayList *tmp2 = controller-&gt;GetCurrentPlayList();
    if(!tmp2)
    {
        tmp1-&gt;Stop();
        return '3';
    }

    if(tmp1 == tmp2)
        tmp1-&gt;Next();
    else
        tmp1-&gt;Stop();
}

return '0';

}
[/quote:ffiq27rb]

When I initialize ForceEnd to true, it doesn’t jump forward 2 songs the first time you call Next(), but the behavior after you call Next() is the same: the callback from that song getting over doesn’t cause it to go to the next song. Any other ideas?

Out of ideas here, :(
James

  • You must to post comments
0
0

Try settings ForcedEnd = true [b:29g5low2]before[/b:29g5low2] you call Stop() in the Next() function.

  • You must to post comments
0
0

Evinyatar,

That seems to take care of the problem with it “jumping” forward 2 songs the first time Next() is called. One behavior problem down, 2 more to go ๐Ÿ˜€

However, there are 2 other problems that happen:
1. If you call Next() and wait on the callback, the callback causes an illegal operation (different than before; before was do nothing).

  1. If you just call Play() and wait on the callback, it causes an illegal operation (same behavior as before).

Thanks for the help everyone, just these last 2 issues are very problematic. :(

James

  • You must to post comments
0
0

I’ve had a look at the code, and I can’t see anything obviously wrong, although I haven’t looked very deeply.

I would imagine the variable you use for the callback parameter if being deleted, and then an attempt is made to use it again.

I would suggest that you use TRACE statements to mark the entry to each relevan function, and the new and delete statements. This should show the flow through the functions, and, one hopes, which one is being called when you do not expect it.

Phil

  • You must to post comments
0
0

Looks like you have are accessing a deleted variable. Consider the following:
[code:k2y41i74]bool PlayList::Play(songInfo *song)
{
/// Omitted code.

sCallBack *param = new sCallBack; 
param-&gt;playlistID = id; 
param-&gt;songID = song-&gt;id; 

FSOUND_Stream_SetEndCallback(currentSound, SoundCallback, (int)param); 

/// Omitted code.

}

signed char SoundCallback(FSOUND_STREAM stream, void *buf, int len, int param)
{
sCallBack *call = (sCallBack
)param;
if(!call)
return ‘1’;

int songID = call-&gt;songID; 
int playID = call-&gt;playlistID; 
delete call; 

/// Omitted code.

} [/code:k2y41i74]
In PlayList::Play() you allocated your sCallBack, and you delete it in the callback. However, if the callback is ever called again, you will get the same parameter. The second time the parameter is invalid. The best way to use “param” is as follows:
[code:k2y41i74]class MyClass
{
private:
// The static callback.
static signed char StaticCallback(FSOUND_STREAM *stream, void *buf, int len, int param)
{
_ASSERTE(param != NULL);
return ((MyClass *)param)->Callback(stream, buf, len, param);
};

public:
    // Enable the callback.
    void SetUpCallback(FSOUND_STREAM *stream)
    {
        FSOUND_Stream_SetEndCallback(stream, MyClass::StaticCallback, (int)this);
    };

protected:
    // The callback.
    signed char Callback(FSOUND_STREAM *stream, void *buf, int len, int param)
    {
        // Do whatever.

        return 0;
    };

};[/code:k2y41i74]
The method MyClass::SetUpCallback uses the “this” pointer so that the object’s callback is called via the static callback. Now inside of MyClass::Callback you can access any of the member data that you need without needing global objects. Obviously this class is grossly incomplete, but I wanted to show you this technique that doesn’t require you to allocate memory.

You asked about disabling the callback… what that means is calling FSOUND_Stream_SetEndCallback() passing NULL for the callback function. But the documentation does not say that this will disable the callback. It might try to invoke the NULL function, which would be bad. Brett, how do you remove a callback?

  • You must to post comments
0
0

I think someone mentioned this already, but in your next function you define, you can set the callback to a dummy function that does nothing, stop, start the next one and reset the callback.

Karg

  • You must to post comments
0
0

Phil, Serac, and Karg,

Thanks for your replies and input! I’ve managed to determine where the problem was and have it fixed so that it goes to the next song on a callback from when the current song ends. :)

However, I found that processor usage jumps from around 16% for the first song to 100% for all songs after the callback. Anyone else run into similar problems? ๐Ÿ˜• The processor usage jump occurs right after the callback function exits. The approach I ended up using was having a static callback method in the PlayList class that calls a dynamic method in the class. I am still using the ForcedEnd approach and it seems to handle things perfectly now. Here is the code for the callback methods:

signed char PlayList::StaticCallback(FSOUND_STREAM *stream, void *buf, int len, int param)
{
return ((PlayList *)param)->Callback();
}

signed char PlayList::Callback()
{
if(ForcedEnd)
{
ForcedEnd = false;
return TRUE;
}

if(this == controller-&gt;GetCurrentPlayList())
{
    currentIndex++;
    if(currentIndex &gt; songs.size() - 1)
        currentIndex = 0;

    Init();
    Play();
}
else
    Stop();

return TRUE;

}

bool PlayList::Init()
{
if ( FSOUND_Init(44100, 32, FSOUND_INIT_GLOBALFOCUS) )
{
FSOUND_SetOutput(FSOUND_OUTPUT_DSOUND);
FSOUND_SetPan(FSOUND_ALL, FSOUND_STEREOPAN);
FSOUND_SetVolume(FSOUND_ALL, 196);
return true;
}

return false;

}

I DID find out why the callback was giving an illegal operation before. It was happening after the callback function exitted, but ONLY if a FSOUND_Stream_Close was called in the callback. The call stack showed this:

FMOD! 1001fe0f()
KERNEL32! bff88f20()
KERNEL32! bff869ef()
KERNEL32! bff868ec()

and here are the register values:

CARMUSICPLAYER caused an invalid page fault in
module FMOD.DLL at 0167:1001fe0f.
Registers:
EAX=1003a71c CS=0167 EIP=1001fe0f EFLGS=00010293
EBX=00000000 SS=016f ESP=00dcff80 EBP=00dcff98
ECX=100391c0 DS=016f ESI=00000001 FS=5c4f
EDX=004219e0 ES=016f EDI=00000000 GS=0000
Bytes at CS:EIP:
8b 06 39 5e 38 89 45 f8 8b 46 04 89 45 f4 74 60
Stack dump:
81ba31e0 00000008 81b6e080 1003a71c 1003a71c 3fee0fe5 00dcffcc bff88f20 00000000 81ba31e0 00000008 81b6e080 00000007 00dcffa4 00dcfdb0 ffffffff

Brett, any ideas why this bombs out when FSOUND_Stream_Close is called in the callback method?

Thanks,
James

  • You must to post comments
Showing 12 results
Your Answer

Please first to submit.